欢迎光临散文网 会员登陆 & 注册

SwiftUI学习100天(Day57 - 项目 12,第一部分)

2023-03-02 12:00 作者:爱上树の蜗牛  | 我要投稿

原创链接:https://www.hackingwithswift.com/100/swiftui

以下内容仅供学习参考: 

在这个技术项目中,我们将探索 Core Data 和 SwiftUI 如何协同工作来帮助我们构建出色的应用程序。我已经向你介绍了项目 11 中的主题,但在这里我们将更详细地介绍:自定义托管对象子类、确保唯一性等。

美国企业家吉姆·罗恩 (Jim Rohn) 曾说过:“成功既不神奇也不神秘——成功是坚持应用基本原则的自然结果。” Core Data 绝对是这些基础知识之一——你当然不会在每个项目中都使用它,但了解它的工作原理以及如何充分利用它将使你成为更好的应用程序开发人员。

今天你有五个主题需要完成,在这些主题中你将了解\.self工作原理、如何仅在需要时保存托管对象上下文、如何确保对象是唯一的,等等。


Core Data:简介

这个技术项目将更详细地探索Core Data,从一些基本技术的总结开始,然后逐步解决一些更复杂的问题。

当你使用 Core Data 时,请记住它已经存在了很长时间——它是在 Swift 出现之前设计的,更不用说 SwiftUI,所以偶尔你会遇到一些不能正常工作的部分正如我们希望的那样,在 Swift 中也是如此。希望我们会在未来几年看到这种情况的改善,但与此同时请耐心等待!

我们有很多 Core Data 需要探索,所以请创建一个我们可以尝试的新项目。称它为“CoreDataProject”而不仅仅是“CoreData”,因为这会导致 Xcode 混淆。

确保你没有选中“Use Core Data”复选框。相反,请从你的 Bookworm 项目中复制 DataController.swift,创建一个名为 CoreDataProject.xcdatamodeld 的新的空数据模型,然后更新 CoreDataProjectApp.swift 以创建一个DataController实例并将其注入 SwiftUI 环境。这将使你离开我们在 Bookworm 项目的早期阶段开始的地方。

重要提示:当你将DataController从 Bookworm 复制到这个新项目时,数据模型文件已经改变——确保你更改NSPersistentContainer初始化程序以引用新的数据模型文件而不是 Bookworm。

随着你的进步,有时你可能会发现 Xcode 会忽略在 Core Data 编辑器中所做的更改,因此我喜欢在转到另一个文件之前按 Cmd+S 来说明这一点。如果失败,请重新启动 Xcode!

可以了,好了?我们走吧!

提示:有时你会看到标题为“想要更进一步?”的标题。这包含一些额外的示例,可帮助你进一步了解知识,但除非你愿意,否则无需遵循此处 - 将其视为额外的学分。

为什么 \.self 为 ForEach 工作?

之前我们研究了ForEach可用于创建动态视图的各种方法,但它们都有一个共同点:SwiftUI 需要知道如何唯一地标识每个动态视图,以便它可以正确地动画更改。

如果对象符合Identifiable协议,则 SwiftUI 将自动使用其id属性进行唯一化。如果我们不使用Identifiable,那么我们可以使用我们知道是唯一的属性的键路径,例如一本书的 ISBN 号。但是如果我们不符合Identifiable并且没有唯一的键路径,我们通常可以使用\.self.

以前我们使用\.self原始类型,例如IntString,如下所示:

有了 Core Data,我们可以根据需要使用唯一的标识符,但我们也可以使用\.self并拥有一些运行良好的东西。

当我们用\.self作标识符时,我们指的是“整个对象”,但实际上这并不意味着什么——结构就是结构,因此除了其内容之外,它没有任何类型的特定标识信息。所以实际发生的是 Swift 计算结构的散列值,这是一种以固定大小的值表示复杂数据的方式,然后使用该散列作为标识符。

哈希值可以通过多种方式生成,但所有哈希生成函数的概念都是相同的:

  1. 无论输入大小如何,输出都应该是相同的固定大小。

  2. 连续两次为一个对象计算相同的散列应该返回相同的值。

这两个听起来很简单,但想一想:如果我们得到“Hello World”的哈希值和莎士比亚全集的哈希值,最终两者的大小将相同。这意味着不可能将哈希值转换回其原始值——我们无法将 40 个看似随机的十六进制字母和数字转换为莎士比亚全集。

哈希通常用于数据验证之类的事情。例如,如果你下载一个 8GB 的 zip 文件,你可以通过将该文件的本地哈希值与服务器的哈希值进行比较来检查它是否正确——如果它们匹配,则意味着该 zip 文件是相同的。哈希也与字典键和集合一起使用;这就是他们快速查找的方式。

所有这些都很重要,因为当 Xcode 为我们的托管对象生成一个类时,它使该类符合Hashable,这是一个协议,这意味着 Swift 可以为其生成哈希值,这反过来意味着我们可以使用\.self标识符。这也是为什么StringInt可以和\.self一起工作:他们也符合Hashable

Hashable有点像Codable:如果我们想让自定义类型符合Hashable,那么只要它包含的所有内容也符合Hashable那么我们不需要做任何工作。为了证明这一点,我们可以创建一个符合Hashable而非的自定义结构Identifiable,并使用\.self它来识别它:

我们可以使Studentcon符合Hashable因为它的所有属性都已经符合Hashable,所以 Swift 会计算每个属性的哈希值,然后将它们组合成一个代表整个结构的哈希值。当然,如果我们最终有两个同名的学生,我们就会遇到问题,就像我们有一个包含两个相同字符串的字符串数组一样。

现在,你可能认为这会导致一个问题:如果我们创建两个具有相同值的 Core Data 对象,它们将生成相同的散列,我们就会遇到动画问题。然而,Core Data 在这里做了一些非常聪明的事情:它为我们创建的对象实际上有一系列超出我们在数据模型中定义的属性的选择,包括一个称为对象 ID 的对象 ID——该对象唯一的标识符,无论它包含什么属性。这些 ID 类似于UUID,尽管 Core Data 在我们创建对象时按顺序生成它们。

所以,\.self适用于任何符合 Hashable的东西,因为 Swift 将为对象生成哈希值并使用它来唯一标识它。这也适用于 Core Data 的对象,因为它们已经符合Hashable. 所以,如果你想使用一个很棒的特定标识符,但你不需要,因为\.self也是一个选项。

警告:虽然连续两次为一个对象计算相同的哈希值应该返回相同的值,但在应用程序的两次运行之间计算它——即计算哈希值、退出应用程序、重新启动,然后再次计算哈希值——可能会返回不同的值。

创建 NSManagedObject 子类

当我们创建一个新的 Core Data 实体时,Xcode 会在我们构建代码时自动为我们生成一个托管对象类。然后我们可以在 SwiftUI 中使用@FetchRequest来在我们的用户界面中显示数据,但正如你所见,这非常痛苦:有很多可选值要解包,因此你需要散布 nil 合并,以使你的代码正常工作。

有两种解决方案:一种快速而简单但有时会出现问题的解决方案,或者一种稍微慢一些但从长远来看效果更好的解决方案。

首先,让我们创建一个要使用的实体:打开你的数据模型并创建一个名为 Movie 的实体,其属性为“title”(字符串)、“director”(字符串)和“year”(整数 16)。在你离开数据模型编辑器之前,我希望你转到“查看”菜单并选择“检查器”>“数据模型”,这会在 Xcode 右侧弹出一个窗格,其中包含有关你现在选择的任何内容的更多信息。

当你选择 Movie 时,你会看到该实体的各种数据模型选项,但我特别希望你查看一个:“Codegen”。这控制了 Xcode 在我们构建项目时如何将实体生成为托管对象类,默认情况下它将是类定义。我想将其更改为手动/无,这使我们可以完全控制课程的制作方式。

现在 Xcode 不再生成一个Movie类供我们在代码中使用,除非我们用一些真正的 Swift 代码实际创建类,否则我们不能在代码中使用它。为此,转到“编辑器”菜单并选择“创建 NSManagedObject 子类”,确保选择“CoreDataProject”,然后按“下一步”,然后确保选择“电影”并再次按“下一步”。系统会询问你 Xcode 应将其代码保存在何处,因此请确保选择左侧带有黄色文件夹图标的“CoreDataProject”,并同时选择 CoreDataProject 文件夹。准备就绪后,按创建以完成该过程。

我们刚刚所做的是要求 Xcode 将其生成的代码转换为我们可以看到和更改的实际 Swift 文件,但请记住,如果你更改 Xcode 为我们生成的文件然后重新生成这些文件,你的更改将会丢失。

Xcode 会为我们生成两个文件,但我们只关心其中一个:Movie+CoreDataProperties.swift。在里面你会看到这三行代码:

在那一小段代码中,你可以看到三件事:

  1. 这就是我们的可选问题的来源。

  2. year不是可选的,这意味着 Core Data 将为我们假设一个默认值。

  3. 它用于@NSManaged所有三个属性。

@NSManaged不是属性包装器——这比 SwiftUI 中的属性包装器要老得多。相反,这揭示了一些 Core Data 的内部工作方式:而不是那些实际作为属性存在于类中的值,它们实际上只是用来从 Core Data 用来存储其信息的字典中读取和写入。当我们读取或写入@NSManaged属性的值时,Core Data 会捕获并在内部处理它——它远非简单的 Swift 字符串。

现在,你可能会查看该代码并认为“我不想要那里的可选值”,并将其更改为:

你知道吗?那绝对有效。你可以使用Movie与以前相同的代码创建对象,使用获取请求来查询它们,保存它们的托管对象上下文等等,所有这些都没有问题。

但是,你可能会注意到一些奇怪的事情:即使我们的属性不再是可选的,仍然可以在Movie不提供这些值的情况下创建类的实例。这应该是不可能的:这些属性不是可选的,这意味着它们必须始终具有值,但我们可以在没有值的情况下创建它们。

这里发生的是一点@NSManaged魔法泄漏——记住,这些不是真实的属性,因此@NSManaged让我们做一些不应该工作的事情。事实上,它确实有效,而且对于小型 Core Data 项目和/或学习者,我认为删除可选性是一个好主意。然而,还有一个更深层次的问题:Core Data 是惰性的。

还记得 Swift 的lazy关键字吗,它如何让我们延迟工作直到我们真正需要它?Core Data 做了很多相同的事情,除了数据:有时它看起来像一些数据已经加载,但实际上并没有加载,因为 Core Data 试图最小化它的内存影响。Core Data 在“断层线”的意义上称这些断层——存在某物的地方和某物只是占位符的地方之间的一条线。

我们不需要做任何特殊的工作来处理这些故障,因为一旦我们尝试读取它们,Core Data 就会透明地获取真实数据并将其发回——这是 @NSManaged的另一个好处。然而,当我们开始研究 Core Data 的属性类型时,我们冒着暴露其特殊弱点的风险。这件事特别不能按照 Swift 预期的方式工作,如果我们试图绕过它,那么我们几乎会引发问题——我们已经说过绝对不会为 nil 的值可能会在任何时候突然变为 nil。

相反,你可能需要考虑添加计算属性以帮助我们安全地访问可选值,同时也让我们将你的 nil 合并代码全部存储在一个地方。例如,将其添加为属性Movie可确保我们始终有一个有效的标题字符串可供使用:

这样你的代码的其余部分就不必担心 Core Data 的可选性,如果你想更改默认值,你可以在一个文件中完成。

NSManagedObjectContext 的条件保存


我们一直在使用NSManagedObjectContext方法save()将所有未保存的更改清除到永久存储中,但我们还没有做的是检查是否确实需要保存任何更改。这通常是可以的,因为save()只有在我们专门进行更改(例如插入或删除数据)之后才发出调用是很常见的。

但是,将你的保存集中在一起以便一次保存所有内容也很常见,这对性能的影响较低。事实上,Apple 特别声明我们应该始终在调用save()之前检查未提交的更改,以避免让 Core Data 执行不需要的工作。

幸运的是,我们可以通过两种方式检查变化。首先,每个托管对象都有一个hasChanges属性,当对象有未保存的更改时该属性为真。并且,整个上下文还包含一个hasChanges属性,用于检查上下文拥有的任何对象是否发生了变化。

因此,与其save()直接调用,不如先将其包装在支票中,如下所示:

这是一个很小的改变,但它很重要——做不需要的工作是没有意义的,无论多么小,特别是因为如果你做了足够多的小工作,你就会意识到你已经积累了一些大工作。


使用约束确保核心数据对象是唯一的

默认情况下,Core Data 会添加任何你想要的对象,但这很快就会变得混乱,特别是当你知道两个或更多对象同时没有意义时。例如,如果你使用电子邮件地址存储联系人的详细信息,那么将两个或三个不同的联系人附加到同一个电子邮件地址就没有意义。

为了帮助解决这个问题,Core Data 给了我们约束:我们可以让一个属性受到约束,这样它就必须始终是唯一的。然后我们可以继续创建任意数量的对象,无论是否唯一,但只要我们要求 Core Data 保存这些对象,它就会解决重复项,以便只写入一个数据。更好的是,如果已经写入了一些与我们的约束冲突的数据,我们可以选择它应该如何处理合并数据。

要尝试这一点,请创建一个名为 Wizard 的新实体,其中包含一个名为“name”的字符串属性。现在选择 Wizard 实体,在数据模型检查器中查找 Constraints,然后按下正下方的 + 按钮。你应该看到“comma,separated,properties”出现,为我们提供了一个工作示例。选择它并按 enter 重命名它,并给它文本“name”代替——这使我们的 name 属性独一无二。请记住按 Cmd+S 保存你的更改!

现在转到 ContentView.swift 并为其提供以下代码:

你可以看到它有一个用于显示向导的列表,一个用于添加向导的按钮和一个用于保存的第二个按钮。当你运行该应用程序时,你会发现你可以多次按添加以查看“哈利波特”滑入表格,但是当你按“保存”时我们会收到一个错误 – Core Data 已检测到碰撞并拒绝保存更改。

如果你希望Core Data 写入更改,你需要打开 DataController.swift 并调整 loadPersistentStores() 完成处理程序以指定在这种情况下应如何合并数据:

这要求 Core Data 根据属性合并重复对象——它会尝试使用新版本的属性智能地覆盖其数据库中的版本。如果你再次运行代码,你会看到一些非常棒的东西:你可以按 Add 多次,但是当你按 Save 时,它会全部折叠成一行,因为 Core Data 删除了重复项。


SwiftUI学习100天(Day57 - 项目 12,第一部分)的评论 (共 条)

分享到微博请遵守国家法律