译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
结合 Core Data 和 SwiftUI
SwiftUI 和 Core Data 几乎是在十年前后被分别引入 —— SwiftUI 是伴随 iOS 13, 而 Core Data 是伴随 iPhoneOS 3 发布;这么早之前设置没有 iOS 的说法,因为 iPad 还没有发布。尽管相隔很久,为了让这两种强大的技术能够完美地协同工作,Apple 做了大量的工作,意味着将 Core Data 集成进 SwiftUI 就像天然设计好的一样。
首先是基础知识:Core Data 是一个“对象图谱和持久化框架”,这是对于定义对象和对象属性,然后从永久存储中读写它们的高级叫法。表面上看,使用它意味着使用Codable和UserDefaults这些东西,但它远不止这些:Core Data 能够排序和过滤我们的数据,对于更大的数据也成立 —— 基本上对于数据量没有限制。更棒的是,Core Data 还实现了各种高级的功能,你会依赖它们:包括数据可视化,数据懒加载,撤销和重做,等等。
在这个项目中我们只会用到 Core Data 强大功能中的一小部分,但我们很快就会扩展更多。现在我希望快速尝试一下。当你创建工程时,我让你把 Use Core Data 的 checkbox 勾选起来,它会给你的项目带来几项改变:
- 你会拥有一个叫 Bookworm.xcdatamodeld 的文件,它描述了你的数据模型,实际上是一张类和它们属性的清单。
- 在 AppDelegate.swift 和 SceneDelegate.swift 有配置 Core Data 的额外代码。
设置 Core Data 需要两步:创建一个“persistent container”,用于从设备存储中加载和保存实际的数据,并且把它注入 SwiftUI 环境,以便所有的视图都能访问它。
以上两步 Xcode 模板都已经为你完成。
因此,剩下要做的事情是决定我们要存入 Core Data 的数据,以及如何读取它们。为了开始这件事,我们需要打开 Bookworm.xcdatamodeld,然后用 Xcode 的模型编辑器描述我们的数据。
之前我们有下面这样一个数据结构:
struct Student {
var id: UUID
var name: String
}不过,Core Data 并不像那样工作,它需要提前知道所有数据的类型,数据里包含的东西,以及数据之间是如何关联的。这正是 “xcdatamodeld” 文件存在的目的:我们定义类型为 “entities”,然后在 entity 里以 “attributes” 的方式创建属性,Core Data 负责把它们转换成运行时实际的数据库结构。
出于尝试的目的,请你点击 Add Entity 按钮来创建一个新的实体,然后双击这个实体重命名为 “Student”。然后,点击 Attributes 表格下点击 + 按钮添加两个 attribute: UUID 的 “id” 和字符串的 “name”。这样就告知了 Core Data 我们创建和保存学生实体的所有信息。现在,回到 ContentView.swift 以继续编码。
从 Core Data 中查询数据是通过一个fetch 请求来完成的 —— 我们描述我们需要的东西,它们应当如何排序,以及是否使用某些过滤器,而 Core Data 会返回所有匹配的数据。我们需要确保这个 fetch 请求随着时间的推移是足够新的,这样我们的界面上的学生信息才能保持同步。
SwiftUI 对此有一个解决方案 —— 你可以猜猜它是什么 —— 它是另一个属性包装器。这个属性包装器称为@FetchRequest,它接收两个参数:我们想要查询的实体,以及我们希望结果执行的排序方式。有一个特定的格式 —— 我们在 students 之前添加 fetch request 注解 —— 请将下面这个属性添加到ContentView:
解释一下,上面这行创建了一个 “Student” 实体的 fetch 请求,并且不应用排序,放进一个叫students,类型为FetchedResults的属性中。
有了这个属性,我们现在像使用常规数组一样使用students,但有一点你需要注意。首先看看下面这段把 students 数组放进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 关注属性在保存时是否有值 —— 它们也可以没有值。
如果你愿意,可以运行代码,不过现在做这个操作还没有意义 —— 列表是空的,因为我们什么数据都还没有添加,所以我们的数据库也是空的。为了解决这个问题,我们将要在列表下方创建一个按钮,每次点击这个按钮时随机添加一个新的学生。在这之前,我们需要一个叫做managed object context的新属性。
让我稍微放慢一下节奏,因为这里要介绍的内容非常重要。当我们定义 “Student” 实体时,实际发生在 Core Data 里的事情是,它为我们创建了一个继承自NSManagedObject的类。我们在代码中看不到这个类,因为它是在我们编译项目时自动生成的,就像 Core ML 的模型。这些对象之所以被称为managed,是因为 Core Data 负责管理它们:它从持久化容器中加载它们,也把它们写回持久化容器。
我们所有的 managed 对象都住在一个managed object context里,它负责实际获取 managed 对象,以及为我们保存变动等等。如果你愿意,可以拥有许多个 managed object contexts ,但我们一般不会这么做 —— 实际上多数情况下一个就够了。
我们并不需要自行创建这个 managed object context,因为 Xcode 已经为我们创建好它了。更好的是,它已经被添加到 SwiftUI 的环境,这也是@FetchRequest属性包装器能工作的原因 —— 它使用环境中可用的任何 managed object context。
尽管如此,当涉及到添加和保持对象时,我们还是需要访问 SwiftUI 环境中的 managed object context。这里是使用@Environment属性包装器的又一个例子 —— 我们可以请求当前的 managed object context,然后赋给一个属性为我们所用。
把这个属性添加到ContentView:
@Environment(\.managedObjectContext) var moc下一步是添加一个生成随机学生并且把它保存到 managed object context 的按钮。为了凸显学生,我们用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()!
// 更多代码
}注意:一定有人会注意到我这里强制解包了randomElement(),但由于我们实际上手写了数组 —— 解包一定会成功。如果你讨厌强制解包,可以替换成空合操作符和默认值。
接下来是有趣的部分:我们将创建一个Student对象 —— 用 Core Data 为我们生成的类,它需要被附着到一个 managed object context,以便对象知道自己应当被存储在哪里。我们可以像操作常规结构体一样给它的属性赋值。
把下面三行代码添加到按钮的 action 闭包:
let student = Student(context: self.moc)
student.id = UUID()
student.name = "\(chosenFirstName) \(chosenLastName)"最后,我们需要让 managed object context 保存这个对象。这是一个可能抛出错误的函数调用,因为理论上它可能失败。实践上,我们做的事情几乎没有机会失败,所以我们可以用try?来调用 —— 我们不关心错误捕获。
把最后一行代码添加到按钮的 action:
try? self.moc.save()现在,你应该运行代码并且尝试了 —— 点击几次 Add 按钮,生成一些随机的学生,你会看到它们从侧面滑入列表。当你重新启动应用时,你会发现这些学生还在那里,因为 Core Data 保存了它们。
我们费了不少力气,了解了 entities 和 attributes 是什么,managed objects 是什么,fetch requests 是什么,以及如何用它们来请求数据和保存变化。在工程后续的部分,我们会继续深入 Core Data。
这一篇是工程概览的最后一部分,现在你可以把代码恢复到初始状态,确保你从我们的数据模型中删除了 Student 实体 —— 我们不再需要它了。
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~