[SwiftUI 100天] Core Data 中实体的对应关系

888 阅读6分钟
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀


Core Data 让我们用关系链接实体。当我们使用@FetchRequest时,Core Data 返回我们要的所有数据。这正是体现出 Core Data 年岁已长的地方之一:为了让关系能够工作,我们需要构建一个自定义的NSManagedObject子类,并且提供对于 SwiftUI 更友好的包装器。

为了说明这一点,我们将构建两个 Core Data 实体:一个用来跟踪糖果,另外一个用于跟踪糖块的原产国。

关系以四种形式呈现:

  • 一对一关系表示某个实体的单一对象精确链接另一个实体的单一对象。在我们的例子中,这可以表示一种糖果有一个原产国,而每个国家只生产一种糖果。
  • 一对多关系表示某个实体的单一对象链接到另一个实体的许多对象。在我们的例子中,这可以表示一种糖果可以同时由多个国家引入,但每个国家仍然只生产一种糖果。
  • 多对一关系表示某个实体的许多对象链接到另一个实体的单一对象。在我们的例子中,这可以表示一种糖果有一个原产国,而每个国家可以生产许多种类的糖果。
  • 多对多关系表示某个实体的许多对象链接到另一个实体的许多对象。在我们的例子中,这可以表示一种糖果可以同时由多个国家引入,并且每个国家可以生产许多种类的糖果。

上面的几种关系在不同的情况下使用,但在我们的糖果例子中,多对一关系最合理 —— 糖果由一个国家发明,但一个国家可以发明很多种糖果。

打开你的数据模型,添加两个实体:Candy,包含字符串属性 “name”,和 Country,包含字符串属性 “fullName” 和 “shortName”。尽管某些糖果可能有相同的名字 —— 比如美国和英国的 “Smarties” —— 国家肯定是唯一的,所以我们要给 “shortName” 添加约束。

提示: 以防你忘记怎么添加约束:选择 Country 实体,到 View 菜单选择 Inspectors > Show Data Model Inspector,点击 Constraints 下的 + 按钮,然后重命名范例文本为 “shortName”。

在完成这个数据模型的编辑之前,我们需要告知 Core Data,在 Candy 和 Country 之间存在多对一的关系。

  • 选中 Country,点击 Relationships 表格下的 + 按钮。给关系取名 “candy”,并把目标改为 Candy,然后在数据模型检视器上把 Type 改成 To Many。
  • 选中 Candy,添加另一个关系。给关系取名 “origin”,并把目标改为 “Country”,然后将它的 inverse 设置为 “candy” 以便 Core Data 知道链接是双向的。

这样就完成了我们的实体,下一步是检查 Xcode 为我们生成的代码。记得点击 Cmd+S,强制 Xcode 保存你的改动。

选中 Candy 和 Country,把它们的 Codegen 都改成 Manual/None,然后到 Editor 菜单,选择 Create NSManagedObject Subclass,给两个实体都创建代码 —— 记得要把它们存放在 CoreDataProject 组和文件夹。

Xcode 会为我们生成四个 Swift 文件。Candy+CoreDataProperties.swift 会跟你预期的一模一样。 Country+CoreDataProperties.swift 更复杂,因为 Xcode 还为我们额外生成一些方法供我们使用。

之前,我们了解过如何用NSManagedObject子类清除 Core Data 的可选型,这里有一个稍微复杂一点额外福利:Country类有一个叫candy的属性,它是一个NSSet,它就旧的 Objective-C 数据类型,等价于 Swift 的Set,但我们不能在 SwiftUI 的ForEach里直接用它。

为了解决这个问题,我们需要修改 Xcode 为我们生成的文件,添加可以完美配合 SwiftUI 的便利包装器。对于Candy类,只需要简单包装name属性,返回字符串就可以了:

public var wrappedName: String {
    name ?? "Unknown Candy"
}

对于Country类的shortNamefullName,做法一样,像这样:

public var wrappedShortName: String {
    shortName ?? "Unknown Country"
}

public var wrappedFullName: String {
    fullName ?? "Unknown Country"
}

不过,对于 candy 情况稍微复杂一些。它是一个 NSSet,可能包含任何东西,因为 Core Data 并没有限制里面的东西必须是 Candy 实例。

因此,为了把这个东西变成对 SwiftUI 有用的形式,我们需要这样做:

  1. 把它从 NSSet 转成 Set —— 一个 Swift 原生类型,我们知道它的内容类型
  2. Set 转成一个数组,以便 ForEach 可以从中读取单独的值
  3. 排序数组,以便糖果以合理的顺序排序。

Swift 实际上可以让我们一步完成 2 和 3,因为排序一个集合会自动返回一个数组。不过,排序数组可能比你想象的要难:这是一个自定义类型的数组,所以我们不能只使用 sorted(),并且让 Swift 搞懂应该怎么排序。相反,我们需要提供一个闭包,接收两个糖果,并且在第一个糖果应该排在第二个糖果前面时返回 true。

因此,把下面这个计算属性添加到 Country

public var candyArray: [Candy] {
    let set = candy as? Set<Candy> ?? []
    return set.sorted {
        $0.wrappedName < $1.wrappedName
    }
}

这样就完成了我们的 Core Data 类,现在我们可以编写 SwiftUI 代码让一切工作起来。

打开 ContentView.swift,添加下面两个属性:

@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Country.entity(), sortDescriptors: []) var countries: FetchedResults<Country>

注意,我们并不需要在 fetch 请求中指定任何关于关系的东西 —— Core Data 知道实体是链接的,它会按需获取它们。

对于视图的 body,我们将用到一个 List,内部包含两个 ForEach:一个用来创建每个国家的段落,另一个用来创建这个国家生成的糖果。这个 List 会放进一个 VStack,我们在它下面放置一个按钮,用于生成范例数据:

VStack {
    List {
        ForEach(countries, id: \.self) { country in
            Section(header: Text(country.wrappedFullName)) {
                ForEach(country.candyArray, id: \.self) { candy in
                    Text(candy.wrappedName)
                }
            }
        }
    }

    Button("Add") {
        let candy1 = Candy(context: self.moc)
        candy1.name = "Mars"
        candy1.origin = Country(context: self.moc)
        candy1.origin?.shortName = "UK"
        candy1.origin?.fullName = "United Kingdom"

        let candy2 = Candy(context: self.moc)
        candy2.name = "KitKat"
        candy2.origin = Country(context: self.moc)
        candy2.origin?.shortName = "UK"
        candy2.origin?.fullName = "United Kingdom"

        let candy3 = Candy(context: self.moc)
        candy3.name = "Twix"
        candy3.origin = Country(context: self.moc)
        candy3.origin?.shortName = "UK"
        candy3.origin?.fullName = "United Kingdom"

        let candy4 = Candy(context: self.moc)
        candy4.name = "Toblerone"
        candy4.origin = Country(context: self.moc)
        candy4.origin?.shortName = "CH"
        candy4.origin?.fullName = "Switzerland"

        try? self.moc.save()
    }
}

确保你运行代码,一切工作正常 —— 点击 Add 按钮的时候,糖果被自动排序插入不同的段落。更棒的是,因为大部分重活是在 NSManagedObject 子类里完成的,实际的 SwiftUI 代码相当的直白 —— 它并不知道幕后的 NSSet,因此要容易理解得多。

提示: 假如你在点击 Add 之后没有看到糖果插入段落,请确保你没有移除 SceneDelegatewillConnectTomethod 方法里的 mergePolicy。以防你忘记了,我指的是这行:context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~