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

SwiftUI学习100天(Day58 - 项目 12,第二部分)

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

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

以下内容仅供学习参考: 

今天我们将推进更高级的Core Data技术——在功能和实用性方面真正让应用程序脱颖而出的技术。其中一些将需要一些时间来学习,特别是因为当我们更多地深入到 Core Data 中时,你将开始更多地看到它的 Objective-C 软肋。

坚持下去!正如 Maya Angelou 所说,“所有伟大的成就都需要时间”——理解 Core Data 在这里为我们所做的一切需要一些工作,但它会得到回报,我相信你会喜欢在你的应用程序。

今天你要完成三个主题,在这些主题中你将了解NSPredicate、动态更改提取请求、创建关系等。

有一次你会看到我说你已经达到了一个很好的点,可以继续学习下一个教程,但如果你继续超越那个点,我们将探索一些更高级的主题。需要明确的是,额外的工作是可选的:如果你时间紧迫,或者只是想打下基础,请不要这样做。

使用 NSPredicate 过滤 @FetchRequest

当我们使用 SwiftUI 的@FetchRequest属性包装器时,我们可以提供一个排序描述符数组来控制结果的排序,但我们也可以提供一个NSPredicate来控制应该显示哪些结果。谓词是简单的测试,测试将应用于我们Core Data实体中的每个对象——只有通过测试的对象才会包含在结果数组中。

NSPredicate的语法不是你可以轻易猜到的,但实际上你只需要几种类型的谓词,所以它并不像你想象的那么糟糕。

要尝试一些谓词,请创建一个名为 Ship 的新实体,它具有两个字符串属性:“name”和“universe”。

现在将 ContentView.swift 修改为:

我们现在可以按下按钮将一些样本数据注入 Core Data,但现在我们没有谓词。为了解决这个问题,我们需要将nil谓词值替换为可以应用于我们的对象的某种测试。

例如,我们可以像这样请求星球大战中的飞船:

如果你的数据包含引号,这会变得很复杂,因此使用特殊语法更常见:`%@' 表示“在此处插入一些数据”,并允许我们将该数据作为参数提供给谓词而不是内联。

所以,我们可以这样写:

除了==,我们还可以使用诸如<>之类的比较来过滤我们的对象。例如,这将返回 Defiant、Enterprise 和 Executor:

%@在幕后做了很多工作,特别是在将原生 Swift 类型转换为 Core Data 等价物时。例如,我们可以使用IN谓词来检查 universe 是否是数组中的三个选项之一,如下所示:

我们还可以使用谓词来检查字符串的一部分,使用诸如BEGINSWITHCONTAINS之类的运算符。例如,这将返回所有以大写字母 E 开头的船只:

该谓词区分大小写;如果你想忽略大小写,则需要将其修改为:

CONTAINS[c]工作方式类似,除了不是以你的子字符串开头,它可以在属性内的任何位置。

最后,你可以翻转谓词使用NOT, 以获得它们常规行为的反转。例如,这会找到所有不以 E 开头的船只:

如果你需要更复杂的谓词,将它们加入使用AND以根据需要建立尽可能多的精度,或者添加一个 Core Data 导入并查看NSCompoundPredicate- 它可以让你从几个较小的谓词中构建一个谓词。

使用 SwiftUI 动态过滤@FetchRequest

我被问到最多的 SwiftUI 问题之一是:如何动态更改核心数据@FetchRequest以使用不同的谓词或排序顺序?出现这个问题是因为获取请求是作为一个属性创建的,所以如果你试图让它们引用另一个属性,Swift 将拒绝。

这里有一个简单的解决方案,回想起来通常很明显,因为它正是其他一切的工作方式:我们应该将我们想要的功能分割成一个单独的视图,然后将值注入其中。

我想用一些真实的代码来演示这一点,所以我把最简单的例子放在一起:它向 Core Data 添加了三位歌手,然后使用两个按钮来显示姓氏以 A 或 S 结尾的歌手。

首先创建一个名为 Singer 的新核心数据实体,并为其提供两个字符串属性:“firstName”和“lastName”。使用数据模型检查器将其 Codegen 更改为 Manual/None,然后转到 Editor 菜单并选择 Create NSManagedObject Subclass 这样我们就可以获得一个Singer我们可以自定义的类。

Xcode 为我们生成文件后,打开 Singer+CoreDataProperties.swift 并添加这两个属性,使该类更易于与 SwiftUI 一起使用:

好的,现在进入真正的工作。

第一步是设计一个视图来承载我们的信息。就像我说的,这也将有两个按钮,让我们改变视图的过滤方式,我们将有一个额外的按钮来插入一些测试数据,这样你就可以看到它是如何工作的。

首先,向你的ContentView结构体添加两个属性,以便我们有一个可以将对象保存到的托管对象上下文,以及一些我们可以用作过滤器的状态:

对于视图的主体,我们将使用VStack带有三个按钮的 ,以及我们希望List显示匹配歌手的位置的注释:

到目前为止,很容易。现在是有趣的部分:我们需要用真实的东西替换那个评论// list of matching singers。这不会被使用@FetchRequest,因为我们希望能够在初始化程序中创建自定义获取请求,但我们将使用的代码几乎是相同的。

创建一个名为“FilteredList”的新 SwiftUI 视图,并为其赋予以下属性:

这将存储我们的获取请求,以便我们可以在body中循环遍历它. 然而,我们不在这里创建获取请求,因为我们仍然不知道我们要搜索什么。相反,我们将创建一个自定义初始化器,它接受一个过滤器字符串并使用它来设置fetchRequest属性。

现在添加这个初始化器:

这将使用当前托管对象上下文运行获取请求。因为这个视图将在内部使用ContentView,所以我们甚至不需要将托管对象上下文注入到环境中——它会从ContentView中继承上下文

你是否注意到开头有一个下划线_fetchRequest?那是故意的。你看,我们不是在获取请求中写入获取的结果对象,而是编写一个全新的获取请求。

要理解这一点,请考虑@State属性包装器。在这个场景的背后,它被实现为一个名为 State的结构,它包含我们放入其中的任何值——例如,一个整数。如果我们有一个@State属性叫做score并将值 10 赋给它,我们的意思是将 10 放入State属性包装器内的整数中。然而,我们也可以给它赋值_score——如果需要的话,我们可以在里面写一个全新的State结构。

因此,通过分配给_fetchRequest,我们并不是要说“这里有一大堆我们希望你使用的新结果”,而是告诉 Swift 我们想要更改整个获取请求本身。

剩下的就是写视图的主体,所以给视图这个主体:

至于FilteredList的预览结构,你可以安全地将其删除。

现在视图已经完成,我们可以返回ContentView并用一些将我们的过滤器传递到的实际代码替换注释FilteredList

现在运行程序试一试:首先点击“添加示例”按钮创建三个歌手对象,然后点击“显示 A”或“显示 S”以在姓氏字母之间切换。你应该看到我们List使用不同的数据动态更新,具体取决于你按下的按钮。

因此,需要一些新知识才能完成这项工作,但实际上并没有那么难——只要你像 SwiftUI 一样思考,解决方案就在那里。

提示:你可能会查看我们的代码并认为每次重新创建视图时——即每次容器视图中的任何状态更改时——我们也在重新创建获取请求,这反过来意味着在没有其他情况时从数据库中读取已经改变。

这可能看起来非常浪费,如果它真的发生了,那将是非常浪费。幸运的是,Core Data 不会做这样的事情:它只会在过滤器字符串更改时重新运行数据库查询,即使重新创建视图也是如此。

想更进一步?

为了获得更大的灵活性,我们可以改进我们的FilteredList视图,使其适用于任何类型的实体,并且可以在任何字段上进行过滤。为了使其正常工作,我们需要进行一些更改:

  1. 我们将使用泛型,而不是专门引用Singer该类,但有一个约束,即传入的任何内容都必须是一个NSManagedObject.

  2. 我们需要接受第二个参数来决定我们要过滤的键名,因为我们可能正在使用没有lastName属性的实体。

  3. 因为我们事先不知道每个实体将包含什么,所以我们将让我们的包含视图来决定。因此,我们不只是使用歌手名字的文本视图,而是要求一个可以运行的闭包来配置他们想要的视图。

里面有两个复杂的部分。第一个是决定每个列表行的内容的闭包,因为它需要使用两个重要的语法。我们在早期关于视图和修饰符的技术项目即将结束时查看了这些内容,但如果你错过了它们:

  • 如果需要,@ViewBuilder让我们的包含视图(无论使用列表的是什么)发送多个视图。

  • @escaping表示闭包将被存储起来并在以后使用,这意味着 Swift 需要照顾好它的内存。

第二个复杂的部分是我们如何让容器视图自定义搜索键。以前我们是这样控制过滤值的:

因此,你可能会进行有根据的猜测并编写如下代码:

但是,那是行不通的。你看,当我们编写%@ Core Data 时,会自动为我们插入引号,以便谓词正确读取。这很有用,因为如果我们的字符串包含引号,它会自动确保它们不会与它添加的引号冲突。

这意味着当我们使用%@属性名称时,我们可能会得到这样的谓词:

这是不正确的:属性名称不应包含在引号中。

为了解决这个问题,NSPredicate有一个特殊的符号可以用来替换属性名称:%K,代表“键”。这将插入我们提供的值,但不会在它们周围添加引号。正确的谓词是这样的:

所以,为 CoreData 添加一个导入,以便我们可以引用NSManagedObject,然后用这个替换你当前的FilteredList结构:

我们现在可以通过ContentView像这样升级来使用新的过滤列表:

请注意我是如何专门用作(singer: Singer)闭包参数的——这是必需的,以便 Swift 了解FilteredList其使用方式。请记住,我们说过它可以是任何类型的NSManagedObject,但为了让 Swift 准确知道它是什么类型的托管对象,我们需要明确说明。

不管怎样,有了这个改变,我们现在可以将我们的列表与任何类型的过滤器键和任何类型的实体一起使用——它更有用了!

与 Core Data、SwiftUI 和 @FetchRequest 的一对多关系

Core Data 允许我们使用关系将实体链接在一起,当我们使用@FetchRequest Core Data 时,会将所有数据发送回给我们使用。然而,这是 Core Data 略显陈旧的一个领域:为了让关系运作良好,我们需要创建一个自定义NSManagedObject子类,提供对 SwiftUI 更友好的包装器。

为了证明这一点,我们将构建两个 Core Data 实体:一个用于跟踪糖果棒,另一个用于跟踪这些棒棒糖的来源国。

关系有四种形式:

  • 一对一关系意味着一个实体中的一个对象恰好链接到另一个实体中的一个对象。在我们的例子中,这意味着每种糖果都有一个原产国,每个国家只能生产一种糖果。

  • 一对多关系意味着实体中的一个对象链接到另一个实体中的许多对象。在我们的例子中,这意味着一种糖果可以同时在许多国家推出,但每个国家仍然只能生产一种糖果。

  • 多对一关系意味着一个实体中的许多对象链接到另一个实体中的一个对象。在我们的示例中,这意味着每种糖果都有一个原产国,并且每个国家可以生产多种糖果。

  • 多对多关系意味着一个实体中的许多对象链接到另一个实体中的许多对象。在我们的示例中,这意味着许多国家同时引入了一种糖果,并且每个国家都可以生产多种糖果。

所有这些都在不同的时间使用,但在我们的糖果示例中,多对一关系最有意义——每种糖果都是在一个国家发明的,但每个国家可以发明多种糖果。

因此,打开你的数据模型并添加两个实体:Candy,带有名为“name”的字符串属性,以及 Country,带有名为“fullName”和“shortName”的字符串属性。尽管某些类型的糖果具有相同的名称(如美国和英国的“Smarties”),但国家/地区绝对是独一无二的,因此请为“shortName”添加一个约束条件。

提示:如果你忘记了如何添加约束,请不要担心:选择 Country 实体,转到 View 菜单并选择 Inspectors > Data Model,单击 Constraints 下的 + 按钮,然后将示例重命名为“shortName”。

在完成此数据模型之前,我们需要告诉 Core Data Candy 和 Country 之间存在一对多关系:

  • 选择国家/地区后,在关系表下按 +。调用关系“candy”,将其目的地更改为 Candy,然后在数据模型检查器中将 Type 更改为 To Many。

  • 现在选择 Candy,并在那里添加另一个关系。将关系称为“来源”,将其目的地更改为“国家/地区”,然后将其倒数设置为“糖果”,以便 Core Data 理解链接是双向的。

这就完成了我们的实体,下一步是查看 Xcode 为我们生成的代码。请记住按 Cmd+S 强制 Xcode 保存你的更改。

选择 Candy 和 Country 并将它们的 Codegen 设置为 Manual/None,然后转到 Editor 菜单并选择 Create NSManagedObject Subclass 为我们的两个实体创建代码——记住将它们保存在 CoreDataProject 组和文件夹中。

当我们选择两个实体时,Xcode 将为我们生成四个 Swift 文件。Candy+CoreDataProperties.swift 几乎完全符合你的期望,尽管请注意origin现在是一个Country. Country+CoreDataProperties.swift比较复杂,因为Xcode也生成了一些方法供我们使用。

之前我们研究了如何使用子类清理 Core Data 的可选值NSManagedObject,但这里有一个额外的复杂性:Country类有一个candy属性是NSSet. 这是旧的 Objective-C 数据类型,相当于 Swift 的Set,但我们不能将它与 SwiftUI 一起使用ForEach

为了解决这个问题,我们需要修改 Xcode 为我们生成的文件,添加使 SwiftUI 运行良好的便利包装器。对于这个Candy类来说,这就像包装name属性一样简单,以便它始终返回一个字符串:

对于这个Country类,我们可以在shortNamefullName周围创建相同的字符串包装器,如下所示:

然而,当涉及到candy时,事情就复杂多了。这是一个NSSet,它可以包含任何东西,因为 Core Data 没有将它限制为Candy.

所以,为了让这个东西变成对 SwiftUI 有用的形式,我们需要:

  1. 将它从一个NSSet转换Set<Candy>——我们知道其内容类型的 Swift 原生类型。

  2. 将其Set<Candy>转换为数组,以便ForEach可以从那里读取单个值。

  3. 对该数组进行排序,使糖果条以合理的顺序排列。

Swift 实际上让我们同时执行步骤 2 和 3,因为对集合进行排序会自动返回一个数组。然而,对数组进行排序比你想象的要难:这是一个自定义类型的数组,所以我们不能只使用sorted()并让 Swift 弄清楚它。相反,我们需要提供一个闭包来接受两个糖果块,如果第一块糖果应该排在第二块之前,则返回 true。

因此,现在请将此计算属性添加到Country

这就完成了我们的Core Data类,所以现在我们可以编写一些 SwiftUI 代码来完成所有这些工作。

打开 ContentView.swift 并赋予它以下两个属性:

请注意我们不需要在我们的获取请求中指定任何关于关系的信息——Core Data 理解实体是链接的,所以它会根据需要获取它们。

至于视图的主体,我们将使用一个List包含两个ForEach视图的 :一个为每个国家/地区创建一个部分,另一个为每个国家/地区创建糖果。这个List将依次进入 VStack因此我们可以在下面添加一个按钮来生成一些示例数据:

请务必运行该代码,因为它运行良好——当点击“添加”按钮时,我们所有的糖果条都会自动分类。更好的是,因为我们在NSManagedObject子类中完成了所有繁重的工作,生成的 SwiftUI 代码实际上非常简单——它不知道幕后有一个NSSet,因此更容易理解。

提示:如果你在按添加后没有看到你的糖果条被分类到不同的部分,请确保你没有从类DataController中删除mergePolicy更改。提醒一下,它应该设置为NSMergePolicy.mergeByPropertyObjectTrump.




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

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