SwiftUI Core Data:一对多关系

\color{red}{\Large \mathbf{Hacking \quad with \quad iOS: SwiftUI \quad Edition}}

{\Large \mathbf{Core \ Data}}

One-to-many Relationships - 韦弦zhy

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”。

Constraints

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

  • 选择Country后,在 Relationships 表下按 +。将该关系称为“candy”,将其 Destination 更改为 Candy,然后在数据模型检查器中将 Type 更改为 To Many。

    Relationships - to many

  • 现在选择 Candy,并在其中添加另一个关系。将关系称为“origin”,将其Destination更改为 Country,然后将其 inverse(反向) 设置为 "candy",以便Core Data理解链接是双向的。


    Relationships - to one

这样就完成了我们的实体,下一步就是看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还生成了一些供我们使用的方法。

以前,我们研究了如何使用NSManagedObject子类清除Core Data的可选内容,但是这里有一个额外的复杂性:Country类具有一个candy属性是NSSet。这是较早的 Objective-C 数据类型,与 Swift 的 Set 等效,但是我们不能在 SwiftUI 的ForEach中使用它。

为了解决这个问题,我们需要修改为我们生成的 Xcode 文件,添加方便包装,以使 SwiftUI 正常工作。对于Candy类,这就像包装名称属性一样容易,这样它总是返回一个字符串:

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<Candy>——一种Swift原生类型,我们知道其内容的类型。
    1. 将该Set<Candy>转换为数组,以便ForEach可以从中读取单个值。
    1. 对那个数组进行排序,使糖果棒明智地排序。

Swift实际上使我们能够一并执行步骤2和3,因为对集合进行排序会自动返回一个数组。但是,对数组进行排序比您想象的要难:这是一个自定义类型的数组,因此我们不能只使用sorted()并让 Swift 来解决它。相反,我们需要提供一个接受两个Candy的闭包,如果第一个糖果应该在第二个糖果之前排序,则返回 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>

请注意,我们不需要在获取请求中指定任何有关关系的信息 —— Core Data理解了链接的实体,因此只需按需获取所有实体即可。

至于视图的主体,我们将使用一个列表,其中包含两个ForEach视图:一个用于为每个国家/地区创建一个分组,一个用于在每个国家/地区中创建糖果。该列表将依次放入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在幕后,因此更容易理解。

提示:如果按添加后看不到排好序的糖果数据,请确保没有从SceneDelegatewillConnectTo方法中删除mergePolicy更改。提醒一下,应该是:context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

译自 One-to-many relationships with Core Data, SwiftUI, and @FetchRequest