在 SwiftData 中使用 @Bindable 的性能问题
在 SwiftData 中直接使用 @Bindable 绑定并更新值,可能导致性能方面的问题。

@Bindable
是从 iOS17 开始引入的一个新的属性包装器,是新的 Observation 框架的一部分,也适用于 SwiftData。
@Bindable 的核心优势
@Bindable 允许我们直接绑定到引用类型对象的属性,而无需创建中间状态变量,可以显著减少样板代码。
要实现类似下面这样修改数据的功能:
0:00
/0:07
为了使用 TextField
来提供用户输入的功能,我们需要为每个字段创建一个临时变量,来存储用户输入的数据,并在保存时更新到 SwiftData 中:
import SwiftData
import SwiftUI
struct EditMovieTheaterInfoSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
var bookmark: Bookmark
// 使用临时变量来存储所有字段的值
@State private var theaterName: String = ""
@State private var showDate: String = ""
@State private var showTime: String = ""
@State private var hall: String = ""
@State private var seat: String = ""
@State private var price: String = ""
// 添加一个日期选择器状态
@State private var selectedDate: Date = Date()
@State private var showDatePicker: Bool = false
var body: some View {
NavigationStack {
Form {
Section("电影院信息") {
TextField("电影院名称", text: $theaterName)
}
Section("放映信息") {
// 日期选择
HStack {
Text("放映日期")
Spacer()
Button(showDate.isEmpty ? "点击选择" : showDate) {
showDatePicker.toggle()
}
.foregroundColor(
showDate.isEmpty ? .secondary : .primary)
}
if showDatePicker {
DatePicker(
"选择日期", selection: $selectedDate,
displayedComponents: .date
)
.datePickerStyle(.graphical)
.onChange(of: selectedDate) { oldValue, newValue in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
showDate = formatter.string(from: newValue)
}
}
// 时间输入
TextField("放映时间 (例如 19:30)", text: $showTime)
}
Section("座位信息") {
TextField("影厅", text: $hall)
TextField("座位", text: $seat)
TextField("票价", text: $price)
.keyboardType(.decimalPad)
}
}
.navigationTitle("观影记录")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("保存") {
// 保存所有临时变量到bookmark对象
bookmark.movieDetails.theaterName =
theaterName.isEmpty ? nil : theaterName
bookmark.movieDetails.showDate =
showDate.isEmpty ? nil : showDate
bookmark.movieDetails.showTime =
showTime.isEmpty ? nil : showTime
// 更新原有字段
bookmark.movieDetails.hall = hall
bookmark.movieDetails.seat = seat
bookmark.movieDetails.price = price
// 保存更改到数据库
try? modelContext.save()
dismiss()
}
}
}
.onAppear {
// 初始化所有临时变量
theaterName = bookmark.movieDetails.theaterName ?? ""
showDate = bookmark.movieDetails.showDate ?? ""
showTime = bookmark.movieDetails.showTime ?? ""
hall = bookmark.movieDetails.hall
seat = bookmark.movieDetails.seat
price = bookmark.movieDetails.price
// 如果有日期,尝试解析为Date对象
if !showDate.isEmpty {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
if let date = formatter.date(from: showDate) {
selectedDate = date
}
}
}
}
}
}
使用 @Bindable 能让代码更加干净,可以使用 TextField 直接绑定 SwiftData 对象的值即可:
import SwiftData
import SwiftUI
struct EditMovieTheaterInfoSheet: View {
@Environment(\.dismiss) private var dismiss
@Bindable var bookmark: Bookmark
// 使用临时变量来存储可选字段的值
@State private var theaterName: String = ""
@State private var showDate: String = ""
@State private var showTime: String = ""
// 添加一个日期选择器状态
@State private var selectedDate: Date = Date()
@State private var showDatePicker: Bool = false
var body: some View {
NavigationStack {
Form {
Section("电影院信息") {
TextField("电影院名称", text: $theaterName)
}
Section("放映信息") {
// 日期选择
HStack {
Text("放映日期")
Spacer()
Button(showDate.isEmpty ? "点击选择" : showDate) {
showDatePicker.toggle()
}
.foregroundColor(showDate.isEmpty ? .secondary : .primary)
}
if showDatePicker {
DatePicker("选择日期", selection: $selectedDate, displayedComponents: .date)
.datePickerStyle(.graphical)
.onChange(of: selectedDate) { oldValue, newValue in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
showDate = formatter.string(from: newValue)
}
}
// 时间输入
TextField("放映时间 (例如 19:30)", text: $showTime)
}
Section("座位信息") {
TextField("影厅", text: $bookmark.movieDetails.hall)
TextField("座位", text: $bookmark.movieDetails.seat)
TextField("票价", text: $bookmark.movieDetails.price)
.keyboardType(.decimalPad)
}
}
.navigationTitle("观影记录")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("保存") {
// 保存临时变量到可选字段
bookmark.movieDetails.theaterName = theaterName.isEmpty ? nil : theaterName
bookmark.movieDetails.showDate = showDate.isEmpty ? nil : showDate
bookmark.movieDetails.showTime = showTime.isEmpty ? nil : showTime
dismiss()
}
}
}
.onAppear {
// 初始化临时变量
theaterName = bookmark.movieDetails.theaterName ?? ""
showDate = bookmark.movieDetails.showDate ?? ""
showTime = bookmark.movieDetails.showTime ?? ""
// 如果有日期,尝试解析为Date对象
if !showDate.isEmpty {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
if let date = formatter.date(from: showDate) {
selectedDate = date
}
}
}
}
}
}
@Bindable 带来的性能考量
使用上述实现时,在 TextField 中输入会出现明显延迟。这是因为:
- 每次对 SwiftData 模型的更新都会触发 SwiftUI 的渲染更新
- 当用户在输入框中键入时,每个字符都会导致一次模型更新
- 这些频繁更新导致 SwiftUI 重新评估视图层次结构,造成性能瓶颈
0:00
/0:03
因此,在 SwiftData 中使用 Bindable 时,并不适合在 TextField 使用。
选择性使用 @Bindable
并非所有 SwiftUI 组件在使用 @Bindable 时都会遇到性能问题,应当根据组件的交互频率和更新方式来选择。
适合直接使用 @Bindable 的组件
以下组件通常不会引起明显的性能问题,适合直接绑定到 SwiftData 模型:
- Picker:由于只在用户完成选择时才更新值,不会产生频繁更新
- Toggle:通常只有两种状态切换,更新频率低
- Stepper:每次点击才更新一次,更新频率较低
- DatePicker(带有合理的交互模式):通常用户会一次性选择日期
- Slider(当设置了 .onEnded 回调时):可以只在滑动结束时更新模型
- ColorPicker:通常一次性选择颜色,不会频繁更新
// 适合直接使用@Bindable的例子
Picker("类别", selection: $bookmark.category) {
ForEach(Category.allCases, id: \.self) { category in
Text(category.rawValue).tag(category)
}
}
Toggle("收藏", isOn: $bookmark.isFavorite)
Stepper("评分: \(bookmark.rating)", value: $bookmark.rating, in: 1...5)
不适合直接使用 @Bindable 的组件
以下组件在直接绑定到 SwiftData 模型时可能会导致性能问题:
- TextField:每个字符输入都会触发更新,导致频繁的模型变更和UI刷新
- TextEditor:和 TextField 类似,会产生高频率的更新
- 实时滑动的 Slider:如果没有使用 .onEnded 回调,会在整个滑动过程中持续更新
- 带有拖动手势的组件:如自定义拖动控件,可能会产生大量更新
- 搜索框:用户快速输入会导致频繁更新
对于这些组件,建议使用中间状态变量:
// 使用中间状态变量的例子
@State private var titleText: String = ""
var body: some View {
TextField("标题", text: $titleText)
.onSubmit {
// 只在提交时更新模型
bookmark.title = titleText
}
}
// 初始化
.onAppear {
titleText = bookmark.title
}