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

SwiftUI学习100天(Day53 - 项目11,第一部分)

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

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

以下内容仅供学习参考: 

今天我们要开始另一个新项目,这就是事情真正开始变得严肃的地方,因为你将学习一项重要的新 Swift 技能、一项重要的新 SwiftUI 技能和一项重要的应用程序开发技能,所有这些都会出现在我们构建项目时很有用。

你将学习的应用程序开发技能是 Apple 的框架之一:Core Data。它负责管理数据库中的对象,包括读取、写入、过滤、排序等,它在 iOS、macOS 及更高版本的所有应用程序开发中都非常重要。以前我们直接将数据写入UserDefaults,但这只是短期的事情,可以帮助你学习 - 核心数据是真正的交易,被数十万个应用程序使用。

加拿大软件开发人员 Rob Pike(Go 编程语言的创造者,开发 Unix 的团队成员,UTF-8 的共同创造者,也是出版作者)写了这篇关于数据的文章:

“数据占主导地位。如果你选择了正确的数据结构并很好地组织了事物,那么算法几乎总是不言而喻的。数据结构,而不是算法,才是编程的核心。”

这通常被简称为“编写使用智能对象的愚蠢代码”,正如你将看到的,对象并没有比在 Core Data 支持下变得更聪明!

今天你有四个主题要完成,你将在其中学习@Binding、类型擦除、Core Data等。

书虫:简介

在这个项目中,我们将构建一个应用程序来跟踪你读过哪些书以及你对它们的看法,它会遵循与项目 10 类似的主题:让我们掌握你已经掌握的所有技能,然后添加一些额外的新技能,使他们都达到一个新的水平。

这次你将遇到 Core Data,它是 Apple 久经考验的数据库处理框架。这个项目将作为 Core Data 的介绍,但我们很快就会进入更多细节。

同时,我们还将构建我们的第一个自定义用户界面组件——一个星级评分小部件,用户可以点击它为每本书留下分数。这将意味着向你介绍另一个属性包装器,称为@Binding- 相信我,这一切都有意义。

像往常一样,我们将从演练开始这个项目所需的所有新技术,因此请使用 App 模板创建一个名为 Bookworm 的新 iOS 应用程序。

重要提示:我知道这很诱人,但请不要选中标有“使用核心数据”的框。它会向你的项目添加一大堆无用的代码,你只需删除它即可继续。

使用@Binding 创建自定义组件

你已经看到 SwiftUI 的@State属性包装器如何让我们使用本地值类型,以及如何@StateObject让我们使用可共享的引用类型。那么,还有第三个选项,称为@Binding,它允许我们将@State一个视图的属性连接到一些底层模型数据。

想一想:当我们创建一个拨动开关时,我们发送了某种可以更改的布尔属性,如下所示:

因此,切换需要在用户与其交互时更改我们的布尔值,但它如何记住它应该更改的值?

这就是@Binding:它让我们在一个视图中存储一个可变值,该视图实际上指向来自其他地方的一些其他值。在 Toggle的情况下,开关将其自身的本地绑定更改为布尔值,但实际上是在幕后操纵我们视图中的@State属性。

这使得@Binding对于任何时候创建自定义用户界面组件都非常重要。从本质上讲,UI 组件就像其他所有内容一样只是 SwiftUI 视图,但@Binding它们与众不同:虽然它们可能具有本地@State属性,但它们也会公开@Binding的属性,让它们直接与其他视图交互。

为了演示这一点,我们将查看创建自定义按钮所需的代码,该按钮在按下时保持按下状态。我们的基本实现都是你以前见过的东西:一个带有一些填充的按钮、一个线性渐变的背景、一个Capsule剪辑形状等等——现在把它添加到 ContentView.swift 中:

唯一令人兴奋的事情是我使用了两种渐变颜色的属性,因此它们可以通过创建按钮的任何内容进行自定义。

我们现在可以创建这些按钮之一作为我们主用户界面的一部分,如下所示:

它在按钮下方有一个文本视图,因此我们可以跟踪按钮的状态——尝试运行你的代码并查看它是如何工作的。

你会发现点击按钮确实会影响它的显示方式,但我们的文本视图并没有反映出这种变化——它总是显示“关闭”。很明显,某些东西正在发生变化,因为按钮的外观在按下时发生了变化,但这种变化并没有反映在ContentView.

这里发生的事情是我们已经定义了一种单向数据流:ContentView有它的rememberMe布尔值,它被用来创建一个PushButton- 按钮有一个由提供的初始值ContentView。但是,一旦创建了按钮,它就会接管该值的控制权:它会在按钮内部将属性isOn在 true 或 false 之间切换,但不会将该更改传回给ContentView.

这是一个问题,因为我们现在有两个事实来源:ContentView存储一个值,PushButton另一个。幸运的是,这就是@Binding派上用场的地方:它允许我们在使用PushButton时和任何事物之间创建双向连接,这样当一个值发生变化时,另一个值也会发生变化。

要切换到@Binding我们只需要进行两个更改。首先,PushButton将其isOn属性更改为:

其次,ContentView改变我们创建按钮的方式:

rememberMe之前添加了一个美元符号——我们传递的是绑定本身,而不是其中的布尔值。

现在再次运行代码,你会发现一切都按预期工作:切换按钮现在也可以正确更新文本视图。

这就是 @Binding的强大之处:就按钮而言,它只是切换一个布尔值——它不知道其他东西正在监视该布尔值并根据变化采取行动。


使用 TextEditor 接受多行文本输入

我们已经多次使用 SwiftUI 的视图 TextField,当用户想要输入短文本时,它非常有用。但是,对于较长的文本片段,你应该改用TextEditor视图:它还希望提供到文本字符串的双向绑定,但它还有一个额外的好处,即允许多行文本——这对接受来自用户的更长的字符串。

主要是因为它在配置选项方面没有什么特别之处,使用TextEditor实际上比使用TextField更容易——你不能调整它的样式或添加占位符文本,你只需将它绑定到一个字符串。但是,你确实需要小心确保它不会超出安全区域,否则打字会很棘手;将它嵌入 一个 NavigationView、 一个Form或相似的。

例如,我们可以通过结合 @AppStorage来创建世界上最简单的笔记应用程序TextEditor,如下所示

提示: @AppStorage并非旨在存储安全信息,因此切勿将其用于任何私人用途。

如何结合 Core Data 和 SwiftUI

SwiftUI 和 Core Data 的推出时间几乎正好相隔十年——iOS 13 的 SwiftUI 和 iPhoneOS 3 的 Core Data;很久以前它甚至不叫 iOS,因为 iPad 还没有发布。尽管时间上相距甚远,Apple 还是投入了大量工作来确保这两项强大的技术能够完美地协同工作,这意味着 Core Data 集成到 SwiftUI 中,就好像它始终以这种方式设计一样。

首先,基础知识:Core Data 是一个对象图和持久性框架,这是一种奇特的说法,它让我们定义对象和这些对象的属性,然后让我们从永久存储中读取和写入它们。

从表面上看,这听起来像是使用Codable UserDefaults,但它比这更高级:Core Data 能够对我们的数据进行排序和过滤,并且可以处理更大的数据——实际上它可以存储多少数据没有限制。更好的是,Core Data 实现了各种更高级的功能,以便在你真正需要依靠它时使用:数据验证、延迟加载数据、撤消和重做等等。

在这个项目中,我们将只使用少量的 Core Data 功能,但很快就会扩展——我只想让你先体验一下。

当你创建你的 Xcode 项目时,我要求你不要选中 Use Core Data 框,因为尽管它去掉了一些无聊的设置代码,但它也添加了一大堆额外的示例代码,这些代码毫无意义而且只是需要被删除。

因此,你将学习如何手动设置 Core Data。它需要三个步骤,从我们定义要在我们的应用程序中使用的数据开始。

之前我们这样描述数据:

然而,Core Data 并不是这样工作的。你看,Core Data 需要提前知道我们所有的数据类型是什么样的,它包含什么,以及它们如何相互关联。

这一切都包含在一个名为数据模型的新文件类型中,它的文件扩展名为“xcdatamodeld”。现在让我们创建一个:按 Cmd+N 创建一个新文件,从模板列表中选择数据模型,然后将你的模型命名为 Bookworm.xcdatamodeld。

当你按下创建时,Xcode 将在其数据模型编辑器中打开新文件。在这里,我们将我们的类型定义为“实体”,然后在其中创建属性作为“属性”——Core Data 负责将其转换为可以在运行时使用的实际数据库布局。

出于试用目的,请按添加实体按钮创建一个新实体,然后双击其名称将其重命名为“学生”。接下来,单击 Attributes 表正下方的 + 按钮添加两个属性:“id”作为 UUID 和“name”作为字符串。

这告诉了 Core Data 创建学生和保存他们需要知道的一切,所以我们可以继续设置 Core Data 的第二步:编写一些 Swift 代码来加载该模型并准备它供我们使用。

我们将把它分成几个小部分来写,这样我就可以详细解释发生了什么。首先import Foundation,创建一个名为 DataController.swift 的新 Swift 文件,并将其添加到其行上方:

我们将首先创建一个名为 DataController的新类,使其符合ObservableObject以便我们可以将它与@StateObject属性包装器一起使用——我们希望在我们的应用程序启动时创建其中一个,然后在我们的应用程序运行期间保持它的活动状态运行。

在这个类中,我们将添加一个类型的属性NSPersistentContainer,它是负责加载数据模型并让我们访问内部数据的核心数据类型。从现代的角度来看,这听起来很奇怪,但“NS”部分是“NeXTSTEP”的缩写,这是 Apple 在 1997 年将史蒂夫·乔布斯带回公司时获得的一个巨大的操作系统——Core Data 有一些非常古老的操作系统基础!

无论如何,首先将其添加到你的文件中:

这告诉 Core Data 我们要使用 Bookworm 数据模型。它实际上并没有加载它——我们稍后会加载它——但它确实准备了 Core Data 来加载它。数据模型不包含我们的实际数据,只是像我们刚才定义的属性和属性的定义。

要实际加载数据模型,我们需要调用我们的容器loadPersistentStores(),它告诉 Core Data 根据 Bookworm.xcdatamodeld 中的数据模型访问我们保存的数据。这样不会把所有的数据同时加载到内存中,因为那样会很浪费,但至少Core Data可以看到我们所有的信息。

加载保存的数据完全有可能出错,例如,数据可能已损坏。但老实说,如果它真的出错了,你也无能为力——此时你能做的唯一有意义的事情就是向用户显示一条错误消息,并希望重新启动应用程序能解决问题。

不管怎样,我们要为DataController写一个小的初始化程序来立即加载我们存储的数据。如果出现问题——不太可能,但并非不可能——我们将向 Xcode 调试日志打印一条消息。

现在将此初始化程序添加到DataController

这就完成DataController了,所以最后一步是创建一个实例DataController并将其发送到 SwiftUI 的环境中。你已经遇到过@Environment要求 SwiftUI 关闭我们的视图,但它还存储其他有用的数据,例如我们的时区、用户界面外观等。

这与 Core Data 相关,因为大多数应用程序一次只能使用一个 Core Data 存储,因此我们不是每个视图都试图单独创建自己的存储,而是在我们的应用启动时创建一次,然后将其存储在 SwiftUI 环境中,这样我们应用程序中的任何其他地方都可以使用它。

为此,打开 BookwormApp.swift,并将此属性添加到结构中:

这创建了我们的数据控制器,现在我们可以通过向该ContentView()行添加一个新的修饰符将其放入 SwiftUI 的环境中:

提示:如果你使用 Xcode 的 SwiftUI 预览,你还应该将托管对象上下文注入到ContentView.

你已经遇到了数据模型,它存储我们要使用的实体和属性的定义,以及NSPersistentStoreContainer处理将我们保存的实际数据加载到用户设备的操作。好吧,你刚刚遇到了 Core Data 难题的第三部分:托管对象上下文。这些实际上是数据的“实时”版本——当你加载对象并更改它们时,这些更改仅存在于内存中,直到你专门将它们保存回持久存储。

因此,视图上下文的工作是让我们使用内存中的所有数据,这比不断地将数据读写到磁盘要快得多。当我们准备就绪时,如果我们希望下次运行应用程序时它们存在,我们仍然需要将更改写入持久存储,但如果你不想要它们,你也可以选择放弃更改。

至此,我们已经创建了我们的 Core Data 模型,我们已经加载了它,并且我们已经准备好与 SwiftUI 一起使用。仍然还有两个重要的难题:读取数据和写入数据。

从 Core Data 中检索信息是使用获取请求完成的——我们描述我们想要什么,应该如何排序,以及是否应该使用任何过滤器,然后 Core Data 发回所有匹配的数据。我们需要确保此获取请求随时间保持最新,以便在创建或删除学生时我们的 UI 保持同步。

SwiftUI 对此有一个解决方案,而且——你猜对了——它是另一个属性包装器。这次它被调用@FetchRequest,它至少需要一个参数来描述我们希望如何对结果进行排序。它有一个特定的格式,所以让我们从为我们的学生添加一个获取请求开始——现在请将这个属性添加到ContentView

分解后,它创建了一个没有排序的获取请求,并将其放入一个名为students的属性中,这个属性的类型是FetchedResults<Student>

从此以后,我们可以像使用常规 Swift 数组一样开始使用students,但是你会看到一个问题。首先,一些将数组放入 一个 List的代码

你发现问题了吗?是的,student.name是一个可选的——它可能有值也可能没有。这是 Core Data 的一个领域,它会让你非常恼火:它有可选数据的概念,但它与 Swift 的可选数据是完全不同的概念。如果我们对 Core Data 说“这东西不能是可选的”(你可以在模型编辑器中这样做),它仍然会生成可选的 Swift 属性,因为 Core Data 所关心的是属性在保存时是否有值– 在其他时候它们可以为零。

如果你愿意,你可以运行代码,但没有什么意义——列表将是空的,因为我们还没有添加任何数据,所以我们的数据库是空的。为了解决这个问题,我们将在我们的列表下方创建一个按钮,每次点击它时都会添加一个新的随机学生,但首先我们需要一个新属性来访问我们之前创建的托管对象上下文。

让我稍微备份一下,因为这很重要。当我们定义“Student”实体时,实际发生的是 Core Data 为我们创建了一个继承于它自己的类:NSManagedObject. 我们在代码中看不到这个类,因为它是在我们构建项目时自动生成的,就像 Core ML 的模型一样。这些对象之所以被称为托管对象,是因为 Core Data 正在照管它们:它从持久容器中加载它们并将它们的更改也写回。

我们所有的托管对象都位于一个托管对象上下文中,我们之前创建了其中之一。将它放入 SwiftUI 环境意味着它会自动用于@FetchRequest属性包装器——它使用环境中可用的任何托管对象上下文。

无论如何,在添加和保存对象时,我们需要访问 SwiftUI 环境中的托管对象上下文。这是@Environment属性包装器的另一个用途——我们可以向它询问当前的托管对象上下文,并将它分配给一个属性供我们使用。

因此,现在将此属性添加到ContentView

有了它,下一步是添加一个按钮,用于生成随机学生并将他们保存在托管对象上下文中。为了帮助学生脱颖而出,我们将通过创建firstNameslastNames数组来分配随机名称,然后使用randomElement()来从每个数组中挑选一个。

首先在List下面添加这个按钮

注意:不可避免地会有人抱怨我强行解包那些对 randomElement()的调用,但我们实际上只是手动创建数组以具有值——它总是会成功。如果你非常讨厌强制解包,也许可以用 nil 合并和默认值替换它们。

现在是有趣的部分:我们将使用我们生成的类 Core Data 创建一个Student对象。这需要附加到托管对象上下文,以便对象知道它应该存储在哪里。然后我们可以像通常为结构体一样为其赋值。

所以,现在将这三行添加到按钮的动作闭包中:

最后,我们需要让我们的托管对象上下文保存自己,这意味着它将把它的更改写入持久存储。这是一个抛出函数调用,因为理论上它可能会失败。在实践中,我们所做的一切都没有任何失败的机会,所以我们可以称之为使用try?——我们不关心捕获错误。

因此,将最后一行添加到按钮的操作中:

最后,你现在应该能够运行该应用程序并尝试一下——点击添加按钮几次以生成一些随机学生,你应该会看到他们滑入我们列表的某个位置。更好的是,如果你重新启动该应用程序,你会发现你的学生仍然在那里,因为 Core Data 保存了他们。

现在,你可能认为这是一个可怕的学习,没有太多结果,但你现在知道什么是持久存储和托管对象上下文,什么是实体和属性,什么是托管对象和获取请求,并且你已经也看到了如何保存更改。我们将在这个项目的稍后部分以及未来更多地关注 Core Data,但现在你已经走得很远了。

这是该项目概述的最后一部分,但这次我不希望你完全重置呢的项目。是的,将 ContentView.swift 恢复到原来的状态,然后从 Bookworm.xcdatamodeld 中删除 Student 实体,但请留下 BookwormApp.swift 和 DataController.swift——我们将在实际项目中使用它们!




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

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