core data stack
SwiftUI
和 Core Data
之间相差将近十年 —— SwiftUI 随着 iOS 13
面世而 Core Data 则是 iPhoneOS 3 的产物;很久以前,它还没有被称为 iOS,因为 iPad 尚未发布。尽管时间相距遥远,Apple
还是投入了大量工作以确保这两种强大的技术能够完美地相互配合使用,这意味着 Core Data
就像始终以这种方式设计一样,已集成到 SwiftUI
中。
在此项目中,我们将仅使用少量 Core Data
的功能,但是这种功能将很快扩展——我只想首先了解一下它。当您创建 Xcode 项目时,我要求您选中 Use Core Data
框,它应该导致对项目的更改:
- 现在,您有了一个名为
Bookworm.xcdatamodeld
的文件。这描述了您的数据模型,该数据模型实际上是类及其属性的列表。 AppDelegate.swift
和SceneDelegate.swift
中现在有用于设置Core Data
的额外代码。
设置核心数据需要两个步骤:创建所谓的持久性容器(从容器存储中加载并保存实际数据),然后将其注入 SwiftUI
环境中,以便我们所有的视图都可以访问它。
Xcode 模板已经为我们完成了这两个步骤。
因此,剩下的就是我们要决定要在 Core Data
中存储哪些数据,以及如何读出这些数据。首先,我们需要打开 Bookworm.xcdatamodeld
并开始使用 Xcode
的模型编辑器描述我们的数据。
之前我们描述过这样的数据:
代码语言:javascript复制struct Student {
var id: UUID
var name: String
}
但是,Core Data
不能那样工作。您会看到,Core Data
需要提前知道我们所有数据类型的样子,包含的内容以及它们之间的关系。这就是 “xcdatamodeld
” 文件的来源:我们将类型定义为“实体”,然后在其中创建属性作为“属性”,Core Data
负责将其转换为可以在运行时使用的实际数据库布局。
为了进行试用,请点击 “Add Entity” 按钮创建一个新实体,然后双击其名称将其重命名为 “Student”。接下来,单击 “Attributes”表正下方的 按钮以添加两个属性:“id”作为 UUID
和 “name” 作为字符串。这将告诉 Core Data
创建学生并保存他们所需的一切,因此请回到 ContentView.swift
,以便我们编写一些代码。
使用获取请求从 Core Data
中检索信息——我们描述了我们想要的内容,应如何对其进行排序以及是否应使用任何过滤器,然后 Core Data
会发回所有匹配的数据。我们需要确保该获取请求随着时间的推移保持最新,以便在创建或删除学生时,我们的 UI 保持同步。
SwiftUI
有一个解决方案,而且——您猜对了——这是另一个属性包装器。这次将其称为@FetchRequest
,它带有两个参数:我们要查询的实体以及我们希望结果如何排序。它具有非常特定的格式,因此,我们首先为学生添加获取请求——请立即将此属性添加到 ContentView
:
@FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults<Student>
分解之后,这创建了一个获取的“学生”实体的请求,不进行任何排序,而是将其放入名称为students
,类型为FetchedResults<Student>
的属性中。
从那里开始,我们可以像常规的 Swift 数组一样开始使用学生,但是您会发现有一个陷阱。首先,一些将数组放入List
的代码:
var body: some View {
VStack {
List {
ForEach(students, id: .id) { student in
Text(student.name ?? "Unknown")
}
}
}
}
}
你发现异常了吗?是的,student.name
是可选的——它可能有一个值,也可能没有。这是 Core Data
的一个领域,该领域会让您大为恼火:它具有可选数据的概念,但与 Swift
的可选数据完全不同。如果我们对 Core Data
说“这不是必须的”(您可以在模型编辑器中完成),它仍然会生成可选的 Swift
属性,因为所有 Core Data
关心的是属性在保存时具有值——在其他时间它们可以为 nil
。
您可以根据需要运行代码,但没有太多意义——该列表将为空,因为我们尚未添加任何数据,因此我们的数据库为空。为了解决这个问题,我们将在列表下方创建一个按钮,每次点击都会添加一个新的随机学生,但是首先我们需要一个新属性来存储托管对象上下文。
让我重申一下,因为这很重要。当我们定义 “Student” 实体时,实际上发生的是 Core Data
为我们创建了一个类,该类继承自其自身的一个类:NSManagedObject
。我们无法在代码中看到该类,因为它是在构建项目时自动生成的,就像 Core ML
的模型一样。这些对象之所以称为托管对象,是因为 Core Data
会照料它们:它从持久性容器中加载它们并将它们的更改也写回。
我们所有的托管对象都位于托管对象上下文中,该上下文负责实际获取托管对象以及保存更改等。如果需要的话,您可以有许多托管对象上下文,但这距离现在还有一段路要走——实际上,您可以长期使用它。
我们不需要创建此托管对象上下文,因为 Xcode
已经为我们创建了一个。更好的是,它已经将其添加到 SwiftUI
环境中,这就是@FetchRequest
属性包装器起作用的原因——它使用了环境中可用的任何托管对象上下文。
因此,现在将此属性添加到ContentView
:
@Environment(.managedObjectContext) var moc
设置好之后,下一步是添加一个按钮,该按钮生成随机的学生并将其保存在托管对象上下文中。为了帮助学生脱颖而出,我们将通过创建firstNames
和lastNames
数组来分配随机名称,然后使用randomElement()
从中选择一个。
首先在List
下方添加此按钮:
Button("Add") {
let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]
let chosenFirstName = firstNames.randomElement()!
let chosenLastName = lastNames.randomElement()!
// more code to come
}
**注意:**不可避免地有人会抱怨我强行对randomElement()
调用,但是实际上我们只是手工创建了具有值的数组——它将永远成功。如果您非常讨厌强制拆包,则可以将其替换为空合计算和默认值。
现在,有趣的部分是:我们将使用为我们生成的 Core Data
类创建一个 Student
对象。这需要附加到托管对象上下文中,以便对象知道应将其存储在何处。然后,我们可以像通常为结构体那样分配值。
因此,现在将这三行添加到按钮的操作闭包中:
代码语言:javascript复制let student = Student(context: self.moc)
student.id = UUID()
student.name = "(chosenFirstName) (chosenLastName)"
最后,我们需要询问托管对象上下文以保存自身。这是一个引发函数的调用,因为理论上它可能会失败。实际上,我们所做的一切都没有失败的可能,因此我们可以使用try?
来调用它——–我们不在乎捕获错误。
因此,请将最后一行添加到按钮的操作中:
代码语言:javascript复制try? self.moc.save()
最后,您现在应该可以运行该应用程序并对其进行尝试——单击几次 “Add” 按钮以生成一些随机的学生,您应该看到他们滑入我们列表的某个位置。更好的是,如果您重新启动该应用程序,您会发现学生还在,因为 Core Data
已保存了他们。
现在,您可能认为这需要大量的学习,但并不会带来很多结果,但是您现在知道什么是实体和属性,知道什么是托管对象和请求,并且已经了解了如何保存更改。在此项目的后面以及将来,我们都将更多地关注 Core Data
,但到目前为止,您已经走了很远。
这是该项目概述的最后一部分,因此,请将您的代码重设为初始状态,并确保您从我们的数据模型中删除了Student
实体——我们不再需要它。
PS: 如果预览报错,那么请跑模拟器
译自 How to combine Core Data and SwiftUI[1]
参考资料
[1] How to combine Core Data and SwiftUI: https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui