我们在讲解 SwiftData 的第一篇文章中,介绍了如何将模型与 SwiftData 结合在一起,第二篇文章讲解了如何在 SwiftUI 中添加数据和查询并展示数据。
下一步就是有趣的部分就是添加一些用户界面,让用户创建、编辑和删除 SwiftData 对象,而不是依赖于示例数据。
删除
其中最简单的是删除,所以我们从这里开始。你可以通过将任何对象传递给模型上下文的 delete() 方法来从 SwiftData中 删除它。
在我们的代码中,我们使用了 ForEach 来遍历 SwiftData 查询返回的所有 person 实例,因此我们现在可以编写与使用 SwiftUI 处理任何数据数组时相同的删除方法。
将此方法添加到 ContentView 中:
func deletePerson(indexSet: IndexSet) {
for index in indexSet {
let person = persons[index]
modelContext.delete(person)
}
}
然后为了模拟删除事件,我们可以给 ForEach 尾部添加以下修饰符:
.onDelete(perform: deletePerson)
点击删除按钮即可删除当前数据,效果图如下:
修改
接下来是编辑数据,这意味着需要使用各种选项创建一个新的 SwiftUI 视图:
- 文本字段,用于编辑目的地的名称和详细信息属性。
- 一个用于调整优先级的选取器。
如果我们把所有这些内容都放入一个表单视图中,那么默认情况下,布局将会非常棒。
所以,现在按 Cmd+N 创建一个新的 SwiftUI 视图,并命名为 EditPersonView。当 Xcode 打开它进行编辑时,请在顶部附近添加 import SwiftData,以便我们可以访问所有的 SwiftData API。
这需要知道选择的 person 实例。如果我们只是想从person 实例读取属性,我们可以像这样添加一个 EditPersonView 属性:
var person: Person
但在这里,仅仅从目标实例对象读取属性是不够的,我们需要能够将它们绑定到 SwiftUI 视图进行数据同步,如 TextField 和 Picker,这样用户就可以实际编辑这些值。
所以,我们需要使用一个名为 @Bindable 的属性包装器,它能够创建任何 SwiftData 对象的绑定。这是为 iOS 17 中引入的 Swift 观察而构建的,但由于 SwiftData 建立在观察的基础上,它在这里也同样有效。上面的代码修改如下:
@Bindable var person: Person
我们添加完这个属性后,会发现我们的预览试图会报错。更糟糕的是,我们不能在预览中创建一个临时 person 实例,因为 SwiftData 不知道在哪里创建它,SwiftData找不到有效的模型容器或上下文。
为了解决这个问题,我们需要手动创建一个模型容器,我们将以一种非常特殊的方式做到这一点:因为这是一个包含示例数据的预览代码,我们将创建一个内存容器,这样我们创建的任何预览对象都不会被保存,而是暂时的。 这需要四个步骤:
- 创建自定义 ModelConfiguration 对象以指定我们想要的内存存储。
- 使用它来创建模型容器。
- 创建包含一些示例数据的示例 person 对象。这将在我们刚刚创建的模型容器内自动创建。
- 将该示例对象和我们的模型容器发送到 EditPersonView,然后全部返回。
在之前的实现中,我们不需要执行步骤1和2,因为这一切都由 SwiftDataDemoApp.swift 中的 modelContainer 修饰符处理,但现在我们需要手动完成,这样我们就可以创建一个 person 对象传递到视图中。
将预览代码修改为:
#Preview {
do {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Person.self, configurations: config)
let example = Person(name: "Mike", address: "Street-3")
return EditPersonView(person: example)
.modelContainer(container)
} catch {
fatalError("Failed to create model container.")
}
}
Tips:如果你尝试创建 SwiftData 模型的实例,但没有有效的模型容器,预览会崩溃。
接下来就是修改 EditPersonView 中的 UI 代码:
@Bindable var person: Person
var body: some View {
Form {
TextField("Name", text: $person.name)
TextField("Address", text: $person.address, axis: .vertical)
Section("Level") {
Picker("Level", selection: $person.level) {
Text("Max").tag(1)
Text("Middle").tag(2)
Text("Small").tag(3)
}
.pickerStyle(.segmented)
}
}
.navigationTitle("Edit Person")
.navigationBarTitleDisplayMode(.inline)
}
效果图如下:
编辑页面写好,接下来就是编写从首页跳转的逻辑:
var body: some View {
NavigationStack {
List {
ForEach(persons) { person in
NavigationLink(value: person) {
VStack(alignment: .leading) {
Text(person.name)
.font(.headline)
Text(person.address)
}
}
}
.onDelete(perform: deletePerson)
}
.navigationDestination(for: Person.self, destination: EditPersonView.init)
.navigationTitle("SwiftDataDemo")
.toolbar { Button("添加数据", action: addData) }
}
}
上述代码的修改是在ForEach的中添加了 NavigationLink,然后在 navigationTitle 下面添加 navigationDestination 修饰符,这样 SwiftUI 就知道在选择目标时跳转到我们的编辑视图了。