购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第2章
关系

在本章中,我们将扩展我们的数据模型。我们会添加两个新的实体:Country(国家)和Continent(大陆)。在这个过程中,我们会解释子实体(subentities)的概念,并且讨论你什么时候应该以及什么时候不应该使用它们。在这之后我们会建立这三个实体之间的关系。关系是Core Data的一个关键特性,我们将使用关系把每个mood和一个country,以及每个country和一个continent联系起来。

2.1 添加Country和Continent实体

修改数据模型会导致App在下次运行时崩溃。但只要你还处于开发阶段而且没有分发App,那么你可以直接删除设备或模拟器里旧版本的App,这样你就可以继续工作了。为简单起见,在本章中,我们假设可以不用担心破坏已有安装而随意地修改数据模型。在第12章会介绍如何在生产环境下处理这个问题。

为了创建Country和Continent这两个新的实体,让我们回到Xcode的模型编辑器里。这两个新的实体都有一个存储country或continent的ISO 3166编码 的属性。我们把这个属性命名为numericISO3166Code,并选择Int16作为它的数据类型。另外,这两个实体都有一个类型为NSDate的updatedAt属性,我们之后在table view里会使用它进行排序。

Country实体的托管对象子类看起来是这样的:

public final class Country: ManagedObject {

@NSManaged internal var updatedAt: NSDate

public private(set) var iso3166Code: ISO3166.Country {

get {

guard let c = ISO3166.Country(rawValue: numericISO3166Code) else {

fatalError("Unknown country code")

}

return c

}

set {

numericISO3166Code = newValue.rawValue

}

}

@NSManaged private var numericISO3166Code: Int16

}

因为numericISO3166Code属性是Country对象如何被持久化的一个实现细节,所以我们把它标记为私有的。我们增加了一个用来公开访问的iso3166Code的计算属性(computed prop-erty),它可以使用枚举类型ISO3166.Country来进行(私有的)设置和读取。ISO3166.Country是使用三个字母的国家码来定义的一个枚举选项:

public struct ISO3166 {

public enum Country: Int16 {

case GUY = 328

case POL = 616

case LTU = 440

//...

case Unknown = 0

}

}

我们还给这个枚举添加了扩展,让枚举的内容可打印,以及便捷地获得一个国家所在的大陆等功能。你可以在示例代码 里查看这个枚举的完整定义。

类似地,Continent类被定义成如下这样:

public final class Continent: ManagedObject {

@NSManaged internal var updatedAt: NSDate

public private(set) var iso3166Code: ISO3166.Continent {

get {

guard let c = ISO3166.Continent(rawValue: numericISO3166Code)

else { fatalError("Unknown continent code") }

return c

}

set {

numericISO3166Code = newValue.rawValue

}

}

@NSManaged private var numericISO3166Code: Int16

}

当然,我们也让Country和Continent类遵循了我们在第1章里介绍过的ManagedObjectType协议。这样一来,新增的类也可以从我们之前添加的便捷方法里受益,比如我们可以用它来插入对象,拿到预先配置好的获取请求等:

extension Country: ManagedObjectType {

public static var entityName: String {

return "Country"

}

public static var defaultSortDescriptors: [NSSortDescriptor]{

return [NSSortDescriptor(key: UpdateTimestampKey,ascending: false)]

}

}

extension Continent: ManagedObjectType {

public static var entityName: String {

return "Continent"

}

public static var defaultSortDescriptors: [NSSortDescriptor]{

return [NSSortDescriptor(key: UpdateTimestampKey,ascending: false)]

}

}

最后,我们引入了一个叫LocalizedStringConvertible的协议。它只有一个只读属性:localizedDescription。通过让Country和Continent这两个类都遵循这个协议,我们有了一个统一使用区域的名字,之后我们可以用它来设置UI中label上的文字:

extension Country: LocalizedStringConvertible {

public var localizedDescription: String {

return iso3166Code.localizedDescription

}

}

extension Continent: LocalizedStringConvertible {

public var localizedDescription: String {

return iso3166Code.localizedDescription

}

}

由于我们存储了country的ISO编码,我们可以使用NSLocale来显示country的本地化名称。而对于continent,我们需要自己提供这个本地化名称。

接下来是把mood和它拍摄时对应的country联系起来。为了做到这一点,我们希望能存储每个mood的地理位置信息。我们将给Mood实体添加两个新的属性:latitude(纬度)和longitude(经度),它们的类型都是Double,因为有时可能会获取不到位置数据,所以两者都是可选的。我们也可以只在一个可转换属性里存储一个CLLocation对象,但是这样会很浪费空间,因为它关联了比我们需要的多得多的数据。所以我们只存储原始的latitude和longitude值,并在Mood类上暴露一个location属性,用这些值我们就可以构造出一个CLLocation对象:

public final class Mood: ManagedObject {

//...

public var location: CLLocation? {

guard let lat = latitude,lon = longitude else { return nil }

return CLLocation(latitude: lat.doubleValue,longitude: lon.doubleValue)

}

@NSManaged private var latitude: NSNumber?

@NSManaged private var longitude: NSNumber?

//...

}

在Mood类里,我们必须要使用NSNumber类型来表示latitude和longitude属性,因为我们希望它们是Optional。我们其实更愿意声明这些属性为Double?,但是这个类型无法在Objective-C里表示,所以没办法和@NSManaged一起工作。

子实体

模型里的实体可以按层次进行组织:一个实体可以是另一个实体的子实体,子实体会继承父实体的属性和关系。虽然这听起来和子类化(subclassing)很相似,但是理解它们之间的差异是很重要的。

创建子实体的唯一原因是,你需要在单一的获取请求的结果或者实体的关系里得到不同类型的对象。在我们的例子里,我们想要用一个table view来将country和continent混合在一起展示,或者每次只展示它们其中一种数据。我们可以通过添加一个抽象的GeographicRegion实体,并让Continent和Country作为它的子实体来实现这个需求。由于Continent和Country共享相同的属性(也就是numericISO3166Code和updatedAt),我们可以把它们移入它们的抽象父实体,如图2.1所示。

图2.1 Xcode模型编辑器里抽象的GeographicRegion父实体

有了这个,我们就能够创建一个使用GeographicRegion实体的获取请求了,它的结果会同时返回country和continent。但是请注意,引入抽象的父实体完全没有改变我们设置托管对象子类的方法。Country和Continent并没有继承一个叫GeographicRegion的共同父类。在我们的例子里,有这样一个父类可能也会很合适,但是实际上并不需要。类的继承关系和实体的继承关系是互相独立的,如图2.2所示。

图2.2 NSManagedObject类层级可以和实体的层级不匹配

理解和避免使用子实体很多时候,你最终会得到多个共用一组属性(比如ID或时间戳)的实体模型。创建一个父实体,只添加一次所有的共同属性的做法看上去很诱人,但是这样会有严重的后果。共同父实体的子实体将共享一个公共的数据库表,所有兄弟实体(sibling entity)的所有属性都会被合并进这个表里,如图2.3所示。尽管Core Data在与你交互的层级上隐藏了这一点,但Core Data将不得不从一个巨大的数据库表里读取所有数据,所以这很快会成为一个性能和内存的问题(如果你不熟悉关系型数据库的结构,可以参考第14章的内容。)

图2.3 Core Data把子实体们合并到一个共同的SQLite表里的方式

可以将子实体想象成是一种给实体添加一个“类型”枚举的取巧的方法,它可以用来告诉你一个实例的类型是“A”还是“B”。当你犹豫是否要使用子实体的时候,请这么思考一下:要是把拥有共同父实体的所有实体合并成一个带有类型属性的单一实体的感觉会不会非常糟糕?如果是,那么你就不应该使用子实体。

除非你需要在相同的获取请求结果或者同一个关系里得到多种类型的对象,否则最好还是避免使用子实体。需要注意的是,多个类继承一个相同的父类,而又不把它们变成相同实体的子实体的做法是完全可以接受的。但是在Swift里,使用一个共同的协议而不是子类化的做法可能会更好。

上面的LocalizedStringConvertible协议就是一个这样的例子,它同时被Country和Continent实现。使用协议允许我们不用继承共同的父类就能使用相同的方式来显示它们的本地化名称。同样地,你可以给所有托管对象类共同的属性定义一个协议——比如一个远程ID或者时间戳属性。

2.2 创建关系

Core Data管理关系的能力是它的核心特性,而且这个特性功能非常强大。我们将在我们所有的三个实体之间创建关系。

我们希望能够使用table view向用户展示一个地理区域的列表。如果用户选择了一个country,那么我们会显示在这个country里拍摄的mood。如果用户选择了一个continent,那么我们将展示这个continent上所有country里拍摄的所有mood。此外,我们还希望能够过滤这个区域列表,让它只显示country或只显示continent。

实体之间的关系很简单:一个continent包含多个country,而每个country只属于一个continent(至少在我们简化版的世界里是这样的)。每个country可以有多个mood,而每个mood只存在于一个country里。这个例子里的关系就是所谓的 一对多 (one-to-many)关系。

我们通常所说的“一对多”的关系,其实是由模型里的两个关系组成的:每个方向各一个。要建立Continent和Country之间的关系,我们实际上在模型编辑器定义了两个关系:一个从Continent到Country,另一个从Country到Continent。Continent上的关系被叫作countries(复数形式,因为它是“对多”的),在Country上的关系被叫作continent(单数形式,因为它是“对一”的)。类似地,我们从Country到Mood建立了一个叫moods的“对多”关系,以及从Mood到Country建立了一个叫country的“对一”关系,如图2.4所示。

图2.4 Continent,Country,以及Mood实体之间的关系

Core Data会自动地更新反向关系:当我们设置Country的continent时,在Continent上对应的countries属性会被自动更新,反之亦然。需要注意的是,反向关系的更新不会立刻发生,而是在上下文的processPendingChanges()方法被调用时更新。你并不需要手动调用这个方法;Core Data会在适当的时候处理好这些事情。更多细节可以参考有关更改和保存数据的章节。

我们也会将这些关系添加到我们的托管对象子类里:

public final class Mood: ManagedObject {

//...

@NSManaged public private(set) var country: Country

//...

}

public final class Country: ManagedObject {

//...

@NSManaged private(set) var moods: Set<Mood>

@NSManaged private(set) var continent: Continent?

//...

}

public final class Continent: ManagedObject {

//...

@NSManaged public private(set) var countries: Set<Country>

//...

}

你可能已经注意到了,Country上的continent关系被标记为可选的。这是因为没有位置信息的mood将会与一个未知country进行关联(我们在ISO3166.Country枚举里定义了一个Unknown),这个未知的country不属于任何一个continent。

在数据模型里定义的这些关系并不是必须要加到你的NSManagedObject子类里的。我们在这里这样做的原因是因为我们希望能在代码里使用它们。只要在数据模型里定义了反向关系,就算子类里没有定义它们,Core Data也会工作得很好。

其他类型的关系

在上面的例子里,我们只用了“一对多”的关系。除此之外,你经常想要在两个实体之间创建的关系要么是“ 一对一 ”,要么是“ 多对多 ”。Core Data直接支持所有这些关系类型,甚至更多的关系类型。

创建一个“一对一”关系和我们在上面创建的“一对多”关系非常类似:创建关系和它对应的反向关系,但这一次在两端都设置关系类型为“ 对一 ”。同样,一个“多对多”关系是通过在两个方向上的关系类型设置为“ 对多 ”来创建的。和“一对多”关系一样,对于“一对一”和“多对多”关系这两种情况,Core Data也会自动更新它们的反向关系。

在内部的SQL存储里,“多对多”关系要比“一对一”或者“一对多”关系更复杂。因为它们用了连接表(join table)。在第14章里会有这方面更详细的内容。

有序关系

“对多”关系有两种形式:无序的和有序的。在默认情况下,“对多”关系没有特定的顺序,通过它们的数据类型就可以看出来。标准的“对多”关系是用Set类型(在Objective-C里是NSSet)的属性来表示的。这可以保证包含的对象的唯一性,但是没有特定的顺序。

当你在模型编辑器选择一个“对多”关系的时候,你可以勾选ordered复选框来改变这个默认行为。有序的关系是用NSOrderedSet类型的属性来表示的,它可以保证包含对象的唯一性以及一个特定的顺序。在下面有关更改“对多”关系一节里,你可以了解在有序关系里插入和移除对象时最好的做法。

Core Data在将有序关系中对象的顺序进行持久化时所使用的底层机制是一个黑盒的实现细节。但是,只要将获取请求的排序描述符的排序键(sort key)指定为这个有序关系的反向关系的名称,我们就可以通过使用这样的获取请求来按有序关系的排序顺序取回这些对象。

其他的使用场景关系并不总是位于两个不同的实体之间。你还可以建立指向一个实体自身的关系,比如你可以通过为同一类实体添加parent和children关系来创建一个树形结构。

另一种并非显而易见的使用场景是在两个实体之间创建多个关系。举一个例子,你有 Country(国家) Person(个人) 两种实体,个人和国家的关联方式有很多种。比如一个人可以是同一个国家或者是不同国家的公民(citizen)和居民(resident)。我们可以用称为residents和citizens的两个“一对多”关系以及对应的反向关系residentOf和citizenOf来建模这种情况。

最后,你还可以创建单向关系(unidirectional relationships),也就是没有对应的反向关系的关系。但是,你应该 非常 小心这种情况,因为这可能导致你的数据集出现参照完整性问题(referential integrity problem)。这意味着在数据库中的一个条目可能指向另一个已经不存在的条目。当你删除一个被其他对象引用,但是却没有指回这些对象的关系的对象时,就可能会发生这种情况。通常情况下,Core Data会保证对象被删除时关系能正确地更新。但一旦你使用了单向关系,那么你就必须要自己处理这些关系的更新。

你应该只有在你完全确信你永远不会删除一个缺少反向关系的对象时才考虑使用单向关系。考虑这个例子:我们有Message和User两种实体,它们是通过从Message到User的一个叫sender的“对一”关系联系起来的。如果我们百分之百确信我们永远不会删除User对象,那么我们可以考虑省去从User到Message的反向关系messages来避免更新这个关系的开销,反正我们永远不会使用它。但要注意,这可能是一个典型的过早优化(premature optimization)的例子——一定要首先检查这个关系是否真的会导致性能问题,再决定是否要做出这样的优化。

建立关系

在我们的例子里,我们想要在一个mood被创建时设置它的country,同时我们希望country被创建时设置它的continent。对于前者,我们可以通过修改Mood类的静态便捷方法来设置country:

public static func insertIntoContext(moc: NSManagedObjectContext,

image: UIImage,location: CLLocation?,placemark: CLPlacemark?) -> Mood

{

let mood: Mood = moc.insertObject()

mood.colors = image.moodColors

mood.date = NSDate()

if let coord = location?.coordinate {

mood.latitude = coord.latitude

mood.longitude = coord.longitude

}

let isoCode = placemark?.ISOcountryCode ?? ""

let isoCountry = ISO3166.Country.fromISO3166(isoCode)

mood.country = Country.findOrCreateCountry(isoCountry,inContext: moc)

return mood

}

在我们把CLPlacemark表示的country代码转换成ISO3166.Country值后(如果代码不能被识别,那么这个值会是.Unknown),我们调用Country类的findOrCreateCountry(_:inCon-text:)方法来获取对应的country对象。这个辅助方法会检查该country是否已存在,如果不存在,则创建它:

static func findOrCreateCountry(isoCountry: ISO3166.Country,

inContext moc: NSManagedObjectContext) -> Country

{

let predicate = NSPredicate(format: "%K == %d",

Keys.NumericISO3166Code.rawValue,Int(isoCountry.rawValue))

let country = findOrCreateInContext(moc,matchingPredicate: predicate) {

$0.iso3166Code = isoCountry

$0.continent = Continent.findOrCreateContinentForCountry(isoCountry,

inContext: moc)

}

return country

}

繁重的工作都是由定义在ManagedObjectType协议的一个扩展里的findOrCreateInContext (_:matchingPredicate:)方法来完成的:

extension ManagedObjectType where Self: ManagedObject {

public static func findOrCreateInContext(moc: NSManagedObjectContext,

matchingPredicate predicate: NSPredicate,

configure: Self -> ()) -> Self

{

guard let obj = findOrFetchInContext(moc,

matchingPredicate: predicate) else

{

let newObject: Self = moc.insertObject()

configure(newObject)

return newObject

}

return obj

}

public static func findOrFetchInContext(moc: NSManagedObjectContext,

matchingPredicate predicate: NSPredicate) -> Self?

{

guard let obj = materializedObjectInContext(moc,

matchingPredicate: predicate)

else {

return fetchInContext(moc) { request in

request.predicate = predicate

request.returnsObjectsAsFaults = false

request.fetchLimit = 1

}.first

}

return obj

}

}

让我们来一步步地分析:首先,我们调用了findOrCreateInContext(_:matchingPredicate:)方法。这里我们会检查我们要寻找的对象是否已经在上下文里注册过。这一步是一个性能优化——因为在我们的例子里,有很大概率我们在之前可能已经加载过这个country对象了。由于获取请求会一路往返到文件系统中去,所以即便是在内存里遍历一个非常大的对象数组,也要比执行一个获取请求快得多。我们将在第6章里对这方面的内容进行更多探讨。

如果我们在上下文里没有找到这个对象,那么我们会尝试使用一个获取请求来加载它。假如这个对象存在于Core Data里,那么它将作为该获取请求的结果被返回。如果它还不存在,那么我们会创建一个新对象,并给这个辅助方法的调用者一个机会来配置这个新创建的对象。

值得一提的是,上面的代码使用了我们的 ManagedObjectType 协议上的两个辅助方法:其中materializedObjectInContext(_:matchingPredicate:) 方法会遍历上下文的registeredObjects集合,这个集合包含了上下文当前所知道的所有托管对象。该方法会一直搜索,直到找到一个不是惰值(faulting)、类型正确、并且可以匹配给定谓词的对象:

extension ManagedObjectType where Self: ManagedObject {

public static func materializedObjectInContext(

moc: NSManagedObjectContext,

matchingPredicate predicate: NSPredicate) -> Self?

{

for obj in moc.registeredObjects where !obj.fault {

guard let res = obj as? Self

where predicate.evaluateWithObject(res)

else { continue }

return res

}

return nil

}

}

这里最重要的地方是,我们在迭代里只考虑那些不是惰值的对象。惰值是指还未填充数据的托管对象实例(更多有关惰值的详情可以参考第4章)。如果我们试图在惰值上执行我们的谓词,就可能会强制Core Data去为每个惰值执行往返于持久化存储的操作,以填充缺失的数据——这种开销可能是非常昂贵的。

ManagedObjectType上的第二个辅助方法可以让我们更容易地执行获取请求。它结合了获取请求的配置和执行,还会把结果转换为正确的类型:

extension ManagedObjectType where Self: ManagedObject {

public static func fetchInContext(context: NSManagedObjectContext,

@noescape configurationBlock: NSFetchRequest -> () = { _ in })

-> [Self]

{

let request = NSFetchRequest(entityName: Self.entityName)

configurationBlock(request)

guard let result = try! context.executeFetchRequest(request)

as? [Self]

else { fatalError("Fetched objects have wrong type") }

return result

}

}

现在,让我们回到修改Mood类之前所试图完成的目标上来。我们已经扩展了Mood的静态辅助方法,现在我们能通过查找一个已存在的country,或者是创建一个新的country对象,并用它来设置Mood上的country关系。对于后面这种情况,我们还需要在新的country对象上设置continent。我们采用和上面为country所做的完全相同的方式来取回这个continent对象:

static func findOrCreateContinentForCountry(isoCountry: ISO3166.Country,

inContext moc: NSManagedObjectContext) -> Continent?

{

guard let iso3166 = ISO3166.Continent.fromCountry(isoCountry)

else { return nil }

let predicate = NSPredicate(format: "%K == %d",

Keys.NumericISO3166Code.rawValue,Int(iso3166.rawValue))

let continent = findOrCreateInContext(moc,

matchingPredicate: predicate) { $0.iso3166Code = iso3166 }

return continent

}

修改“对多”关系

在上面的例子里,我们只从“对一”方向通过直接设置关系的另一边上的对象属性建立了我们的“对多”关系。当然,你也可以在另一端修改一个关系,也就是修改关系的“对多”方向的对象。要做到这一点的最直接的方式是,拿到这个关系属性的可变集合,然后做你想要的更改。

例如,我们可以在Country类里添加下面这个私有属性来修改moods关系(在我们的例子里我们并不需要这么做,但是出于演示的目的我们包含了这个属性):

private var mutableMoods: NSMutableSet {

return mutableSetValueForKey(Keys.Moods)

}

这个moods关系对于其他地方仍然只被公开为一个不可变的集合,但在内部,我们可以使用这个可变版本来改变关系,比如,可以添加一个新的mood对象:

mutableMoods.addObject(mood)

同样的方法也适用于有序的“对多”关系。你只需要使用mutableOrderedSetValueForKey(_:)而不是mutableSetValueForKey(_:)方法就可以了。

值得一提的是,Xcode的NSManagedObject子类代码生成器创建的辅助方法实际上在有序关系上并不能工作。但是正如我们看到的,这并不能阻止你使用有序关系。使用可变的(有序的)set方法往往更简单有效,这也是我们所推荐的做法。

关系和删除

关系在删除过程中发挥着特殊的作用:当你在删除有指向另一个对象的关系的对象时,你需要决定应该如何处理关联的对象。例如,当一个country对象被删除时,Core Data需要更新相应continent对象上的countries关系来对更改做出响应。为了实现这个目标,我们设置country的continent关系的删除规则为 nullify(置空) 。这会导致关联的对象——在我们的例子里,continent会被保留,而它的反向关系countries会被更新,如图2.5所示。

图2.5 nullify删除规则会把被删除的对象从它的反向关系里移除

删除规则也可以被设置为 cascade(级联) ,这会导致在另一端的对象(们)也被删除。虽然在我们的具体例子里我们没有这么做,但在Continent上的countries关系采取这种规则可能是合理的。比如,当一个continent对象被删除时,我们可能希望Core Data也删除所有相关的country对象,如图2.6所示。

事实上,只要分别还存在关联的country和mood对象,我们就希望保证对应的continent和country对象不会被删除。Core Data还有另一个删除规则可以保证这一点: deny(拒绝) 。在continent的countries关系上将删除规则设置为deny的话,那么只要仍然存在相关联的country,我们尝试删除continent对象时就将失败,如图2.7所示。

图2.6 cascade删除规则会删除相关联的对象们

图2.7 deny删除规则可以防止关系不为空的对象被删除

最后一个删除规则, no action(无动作) ,应该被小心地使用:因为这意味着Core Data不会更新反向关系(们),而是由我们开发者向Core Data保证我们已经准备好了更新它们的自定义代码。

自定义删除规则

有时候你希望使用一种和Core Data提供的删除规则都不同的删除行为。比如在我们的例子里,我们希望清理不再引用任何mood的country对象,以及不再引用country的continent对象。我们可以通过对Country类的prepareForDeletion()方法进行挂钩(hooking)来实现这个需求:

public final class Country: ManagedObject {

//...

public override func prepareForDeletion() {

guard let c = continent else { return }

if c.countries.filter({ !$0.deleted }).isEmpty {

managedObjectContext?.deleteObject(c)

}

}

//...

}

这个方法会在对象被删除之前被调用。在该方法里,我们可以检查continent的countries关系是否仍然包含未删除的country对象。如果没有未删除的country对象,那么我们会删除这个continent。我们在Mood类里可以用同样的方法来删除不再引用任何mood的country对象。

2.3 适配用户界面

为了在UI上展示country和continent。我们添加了另一个table view controller。我们会把它插入到导航栈中用于展示mood对象的view controller之前的位置。这个table view将country和continent显示在一个组合列表里。另外,它还有一个过滤选项来让列表只显示continent或者country,如图2.8所示。

在这个table view controller里,我们使用了我们在第1章里介绍的和展示mood table view相同的通用data provider和data source类。我们通过在region table view controller的viewDidLoad()方法里调用如下方法来进行设置:

private func setupDataSource() {

let request = filterSegmentedControl.regionFilter.fetchRequest

let frc = NSFetchedResultsController(fetchRequest: request,

managedObjectContext: managedObjectContext,

sectionNameKeyPath: nil,cacheName: nil)

let dataProvider = FetchedResultsDataProvider(

fetchedResultsController: frc,delegate: self)

dataSource = TableViewDataSource(tableView: tableView,

dataProvider: dataProvider,delegate: self)

}

图2.8 在示例应用程序里的region table view

这里有趣的地方是我们为导航栏里选中的segment创建获取请求的方式。在mood table view controller里,我们只是直接使用我们在Mood类上的便捷属性sortedFetchRequest。但是现在的情况有所不同:我们要展示country或continent这两个实体的其中一个,或者是同时展示它们。

首先,我们创建了一个用来表示用户在segmented控件里选择的不同过滤选项的枚举:

private enum RegionFilter: Int {

case Both = 0

case Countries = 1

case Continents = 2

}

然后,我们给UISegmentedControl(我们用来选择应该显示哪个区域的控件)添加一个扩展。这个扩展根据它被选中索引的返回一个RegionFilter值:

extension UISegmentedControl {

private var regionFilter: RegionFilter {

guard let rf = RegionFilter(rawValue: selectedSegmentIndex) else {

fatalError("Invalid filter index")

}

return rf

}

}

最后,我们扩展RegionFilter枚举来添加一个fetchRequest属性,它会为当前选中的region返回恰当的获取请求:

extension RegionFilter {

var fetchRequest: NSFetchRequest {

var request: NSFetchRequest

switch self {

case.Both: request = GeographicRegion.sortedFetchRequest

case.Countries: request = Country.sortedFetchRequest

case.Continents: request = Continent.sortedFetchRequest

}

request.returnsObjectsAsFaults = false

request.fetchBatchSize = 20

return request

}

}

对于Countries和Continents的情况来说很简单,但是Both的情况现在还不能正常工作——我们甚至还没有定义GeographicRegion类型。为了在一个获取请求里同时得到country和continent对象,我们可以将它们的抽象父实体指定为我们在数据模型里创建的GeographicRegion。我们定义了一个遵循ManagedObjectType协议的GeographicRegion类型,这让我们能够用和处理Country和Continent一样的方式来调用GeographicRegion.sort-edFetchRequest:

public class GeographicRegion: ManagedObject {}

extension GeographicRegion: ManagedObjectType {

public static var entityName: String { return "GeographicRegion" }

public static var defaultSortDescriptors: [NSSortDescriptor]{

return [NSSortDescriptor(key: "updatedAt",ascending: false)]

}

}

现在我们不必对这种情况作出区分了,实体名称也被很好地封装了。

接下来,让我们为我们的通用data source实现cellIdentifierForObject(_:)这个代理方法。因为table view要显示不同类型的对象,也就是Country和Continent,所以出现了我们应该如何在代理协议里指定Object类型别名的问题。我们可以使用NSManagedObject,然后尝试把对象转换成Country或Continent来弄清楚我们正在使用什么对象。但是在这里我们将采取另外一种不同的方法,通过引入另一个协议来简化委托代码:

protocol DisplayableRegion: LocalizedStringConvertible {

var reuseIdentifier: String { get }

var localizedDetailDescription: String { get }

var segue: RegionsTableViewController.SegueIdentifier { get }

}

我们通过在Country和Continent上实现reuseIdentifier和segue属性来让它们遵循这个协议:

extension DisplayableRegion {

var reuseIdentifier: String { return "Region" }

}

extension Country: DisplayableRegion {

var localizedDetailDescription: String {

return localized(.Regions_numberOfMoods,args: [numberOfMoods])

}

var segue: RegionsTableViewController.SegueIdentifier {

return.ShowCountryMoods

}

}

extension Continent: DisplayableRegion {

var localizedDetailDescription: String {

return localized(.Regions_numberOfMoodsInCountries,

args: [numberOfMoods,numberOfCountries])

}

var segue: RegionsTableViewController.SegueIdentifier {

return.ShowContinentMoods

}

}

此外,我们还在RegionTableViewCell的一个扩展里实现了ConfigurableCell协议,就像我们在第1章为mood cell做的那样:

extension RegionTableViewCell: ConfigurableCell {

func configureForObject(object: DisplayableRegion) {

titleLabel.text = object.localizedDescription

detailLabel.text = object.localizedDetailDescription

}

}

做完这些之后,data source里的委托方法就很简单了:

extension RegionsTableViewController: DataSourceDelegate {

func cellIdentifierForObject(object: DisplayableRegion) -> String {

return object.reuseIdentifier

}

}

在完整的示例项目中,我们更进一步,在region table view前面添加了一行额外的“All Moods”。为了做到这一点,我们添加了另外一个基于FetchedResultsDataProvider构建的data provider类,它允许我们通过其代理来指定一些补充的行,而这些行并不是获取结果的一部分。你可以参考GitHub 上有关这部分的代码。

2.4 总结

在本章中,我们增加了两个新的实体,Country和Continent,并在它们之间建立了关系:一个continent包含一个或多个country,一个country包含一个或多个mood。我们在两个方向上定义了这些关系,比如从country到mood,以及从mood到country。在我们建立或打破两个对象之间的关系的时候,Core Data会自动更新对应的反向关系。此外,Core Data也会根据我们在关系上设置的删除规则来传播或者阻止对于关联着其他对象的对象的删除。

有了这些新的实体和关系之后,我们更新了插入新mood的便捷方法,如果不存在相应的country和continent,那么这个方法会自动创建它们。作为一个性能优化,我们在回退去使用较慢的获取请求之前,首先对上下文里已注册的对象进行遍历,来检查一个country或continent是否已经存在。

重点

·仅在你能合理地用一个枚举属性来把实体们合并成一个实体的时候才使用子实体。

·Core Data可以处理“一对一”,“一对多”,以及“多对多”关系。

•“对多”关系有两种形式:无序的和有序的。

·一个实体可以有指向自身的关系,比如用parent属性来创建一个树结构。

·两个实体可以通过多个关系来连接。

·确保为你的使用场景下的关系设置合适的删除规则。

·使用mutableSetForKey(_:)或者mutableOrderedSetForKey(_:)存取方法来修改“对多”关系。 MXL+fa/SJLDcsdQliq6ODwdX8YqxQjxIkAuKLqDav5YaGhPKBvk5NhxlpU6JAl3R

点击中间区域
呼出菜单
上一章
目录
下一章
×