Core Data(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-08 12:10:56

点击下载

作者:Florian Kugler(佛罗莱恩·库格勒)

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Core Data

Core Data试读:

前言

中会有所说明,这里就不再赘述了。需要补充的是,本书里提供了大量的例子和相应的代码,它们大多是需要进行权衡的选择,并对应了不同的场景。只有在你充分理解这些例子的含义后,你才可能在实际使用时做出正确的判断。另外,Core Data的灵活性是一把双刃剑,当你选择了更多的上下文以及协调器时,也意味着你为项目引入了更多的复杂度。尽可能在能够满足需求的前提下,选择最简单的Core Data栈设置,是高效、正确使用Core Data的关键。本书原著的两位作者有着多年的Core Data使用经[1]验。Florian Kugler是objc.io的联合创始人,曾经为objc.io撰写了很[2]多Core Data相关的文章,深受读者喜爱。Daniel Eggert曾供职于Apple,帮助Apple将照片应用迁移到Core Data框架内。他们的努力让Core Data这个看起来有些“可怕”的框架变得平易近人,借此我们可以一窥Core Data的究竟。不过不论是原作者还是译者,其实和各位读者一样,都只不过是普通开发者中的一员,所以本书出现谬漏可能在所难免。如果你在阅读时发现了问题,可以通过出版社联系我们,我们将及时研究并加以改进。

最后,祝你阅读愉快。徐涛钱世家王巍

[1]https://twitter.com/floriankugler

[2]https://twitter.com/danielboedewadt?lang=en前言

Core Data是Apple为iOS、OS X、watchOS和tvOS而设计的对象图管理(object graph man-agement)和数据持久化框架。如果你的App需要存储结构化的数据,那么Core Data是一个显而易见的方案:它是现成的,Apple仍然在积极地维护它,而且它已经存在超过10年了。Core Data是一个成熟、经过实践检验的代码库。

然而Core Data最初会让人有一些困惑:它非常灵活,但是API的最佳实践却并非显而易见。换句话说,本书的目标是帮助读者快速入门Core Data。我们希望提供给读者一系列包括从简单到高级的使用场景中的最佳实践,这样你可以充分利用Core Data的能力而又不会迷失在一些不必要的复杂性中。

比如,Core Data经常被诟病难以在多线程环境中使用。其实Core Data的并发模型非常明确和一致。如果正确使用,那么它可以帮助你避免许多并发编程中一些固有的陷阱。其他的复杂性并不是由Core Data引入的,它们的根源其实是并发本身。我们会在第9章中对其进行深入研究,另外我们还会实际演示一个后台同步方案的例子。

除此之外,Core Data也经常被吐槽性能糟糕。如果你像使用关系型数据库那样来使用Core Data,那么你会发现与直接使用类似SQLite这样的数据库相比,Core Data的性能开销会很高。但如果把Core Data当成一个对象图管理系统来正确使用,那么得益于内建的缓存和对象管理机制,它在很多方面实际上反而更快。此外,抽象级别更高的API可以让你专注于优化App里关键部分的性能,而不是从头开始来实现如何持久化。在本书中,我们会介绍保持Core Data高性能的最佳实践,并在专门讲性能以及性能分析的章节中探讨如何解决Core Data的性能问题。

本书使用Core Data的方式

本书展示了如何在实际例子中使用Core Data,而不仅仅是简单地对API手册进行一些扩展。我们有意专注于完整例子的最佳实践。根据我们的经验,正确地组合使用Core Data的各个部分往往是最大的挑战。此外,本书还深入解释了Core Data内部的运作原理。了解Core Data这个灵活框架可以帮助你做出正确的决定,同时能让你的代码保持简单易懂。特别是当遇到并发和性能问题时,这一点尤为重要。

示例代码[1]

你可以在GitHub上找到一个完整的示例程序的源代码。我们在本书中很多地方都将用这个示例程序来演示Core Data在较大的项目中面临的挑战和相应的解决方案。

请注意该示例程序代码有时会和本书前面的一些章节中的示例程序有所不同。因为示例项目是最终形态的完整的代码,而本书前面章节中描述的是该示例程序早期、简单阶段的代码。

结构

在本书的第一部分,我们会创建一个简单版本的应用程序,来演示如何使用Core Data以及Core Data的基本工作原理。即使早期的示例对读者来说可能相当容易,但我们仍然建议读者浏览本书的这些部分,因为后面更复杂的例子是建立在前面介绍的最佳实践和技术基础之上的。我们还想告诉你的是,即便在简单的应用场景中,Core Data也会非常有用。

第二部分则着重深入介绍Core Data各个部分是如何一起协作的。我们会仔细探讨当以不同方式访问数据时会发生什么,我们也会对插入或者操作数据时发生的情况进行研究。这部分所覆盖的内容会比写一个简单的Core Data应用程序所必要得多,这些方面的知识在处理更大或更复杂的情况时可以派上用场。在此基础上,我们将以性能方面的考量来对这个部分进行总结。

第三部分从描述一个用来保持本地数据与网络服务一致的通用同步架构开始,然后我们会深入探讨如何在Core Data中同时使用多个托管对象上下文(managed object context)。我们提出设置Core Data栈的不同方案,并讨论了它们的优缺点。在第9章里,介绍了如何应对同时使用多个上下文带来的额外复杂性。

第四部分涉及一些高级的主题,比如高级的谓词(predicate)、搜索和文本排序、如何在不同的数据模型版本之间迁移数据,以及分析Core Data栈的性能时所需要的工具和技术等。这部分中有一章是从Core Data视角介绍有关关系数据库和SQL查询语言的基本知识的。如果你不熟悉这些内容,那么这些章节能对你有所帮助,特别是可以让你理解Core Data潜在的性能问题,以及解决这些问题所需要的分析技术。

关于Swift的一些说明

贯穿本书,我们所有的示例都使用Swift编写。我们拥抱Swift的语言特性——比如泛型、协议以及扩展——它们能让我们更优雅、简单、安全地使用Core Data的API。

用Swift表示的最佳实践和设计模式同样也适用于Objective-C的代码。在实现上,由于语言上的不同,或许在某些方面会稍有不同,但是底层的原则是相通的。

可选值的约定

Swift提供了Optional数据类型,这迫使我们显式地思考和处理没有值的情况。我们非常喜欢这个功能,所以我们在所有的例子里都使用了它。

因此我们尽量避免使用Swift的! 操作符来强制解包(包括用它来定义隐式解包类型的用法),在我们看来这是一种坏代码的味道,因为它破坏了我们使用可选值类型所带来的类型安全。

唯一的例外是那些必须设置但又无法在初始化时设置的属性。比如Interface Builder的outlets或必要的代理(delegate)属性等。在这些情况下,使用隐式解包的可选值符合“尽早崩溃”原则:我们会立刻知晓这些必须要设置而又没有正确设置的属性。

错误处理的约定

Core Data中许多方法会抛出错误。基于它们是不同类型的错误这一基本事实,我们可以分类处理这些错误。我们将区分逻辑错误和其他错误。

逻辑错误是指程序员犯错的结果。它们应该从代码层面上修复而不应该尝试动态恢复程序的运行。

举一个例子,当你尝试读取应用程序包里的一个文件时,因为应用程序包是只读的,那么一个文件要么存在,要么不存在,而且它的内容永远不会变。所以如果我们无法打开或者解析应用程序包里的文件,那么这就是一个逻辑错误。

对于这些类型的错误,我们使用Swift的try! 或fatalError()来尽可能早地让应用程序崩溃。

同样的思想可以适用于as! 操作符的强制类型转换:如果我们知道一个对象必须是某种类型,转换失败的唯一原因会是逻辑错误,那么在这种时候我们实际上是希望应用程序崩溃的。

很多时候我们用Swift的guard关键字来更好地表达哪些地方出错了。例如fetched results controller返回的类型是NSManagedObject的对象,我们知道它必须是一个特定的子类,我们使用guard来保证向下转换,并在出错的时候使用fatal error来中止程序:

func objectAtIndexPath(indexPath: NSIndexPath) -> Object {

guard let result = fetchedResultsController.objectAtIndexPath(indexPath)

as? Object else

{

fatalError("Unexpected object at \(indexPath)")

}

return result

}

对于可恢复的非逻辑性错误,我们使用Swift的错误传递方法:抛出(throw)或者重新抛出(rethrow)这些错误。

Florian

Daniel

[1]https://github.com/objcio/core-dataⅠ Core Data基础第1章初探Core Data

在本章中,我们将创建一个简单的使用Core Data的示例程序。在这个过程中,我们会介绍Core Data的基本架构以及在此场景下如何正确使用它。当然,本章提到的方方面面都有更多值得一谈的内容。不过请放心,后面我们会详细回顾这些内容。

本章会介绍这个示例程序中与Core Data相关的所有方面的内容。请注意这并不是一个从头开始一步步教你如何创建整个应用的教[1]程。我们推荐你看一下在GitHub上完整的代码来了解在实际项目中不同的部分。

这个示例应用程序包括一个简单的table view和底部的实时摄像头拍摄的内容。拍摄一张照片后,我们从照片中提取出它的一组主色。然后存储这些配色方案(我们称其为“mood”),并相应地更新table view,如图1.1所示。1.1 Core Data架构

为了更好地理解Core Data的架构,在我们开始创建这个示例应用之前,让我们先来看一看它的主要组成部分。在本书第二部分中会详细介绍所有这些部分是如何协同工作的。

一个基本的Core Data栈由四个主要部分组成:托管对象(managed objects)(NSManagedObject)、托管对象上下文(managed object context)(NSManagedObjectContext)、持久化存储协调器(persistent store coordinator)(NSPersistentStoreCoordinator),以及持久化存储(persistent store)(NSPersistentStore),如图1.2所示。

托管对象位于这张图的最上层,它是架构里最有趣的部分,同时也是我们的数据模型——在这个例子里,它是Mood类的实例们。Mood需要是NSManagedObject类的子类,这样它才能与Core Data其他的部分进行集成。每个Mood实例表示了一个mood,也就是用户用相机拍摄的照片。图1.1 示例应用程序——“Moody”图1.2 Core Data栈的基本组成部分

我们的mood对象是被Core Data托管的对象。也就是说,它们存在于一个特定的上下文(context)里:即托管对象上下文。托管对象上下文记录了它管理的对象,以及你对这些对象的所有操作,比如插入、删除和修改等。每个被托管的对象都知道自己属于哪个上下文。Core Data支持多个上下文,但是我们先别好高骛远:先像本章中最简单的设置这样,只使用一个单独的上下文。

上下文与持久化存储协调器相连,协调器位于持久化存储和托管对象上下文之间,正如其名,它起到协调者的作用。和上下文类似,你也可以使用多个持久化存储和持久化存储协调器的组合。不过你很少需要这么做。现在,我们只会使用一个上下文、一个持久化存储协调器和一个持久化存储。

持久化存储协调器是位于Core Data栈正中间的一个黑盒对象,通常你不会和它直接打交道。但是它又是一个非常重要的部分,在本书的第4章中会详细讨论有关持久化存储协调器的内容。

最后一部分就是持久化存储了,它是持久化存储协调器的一部分(一个NSPersistentStore实例与一个特定的协调器相绑定),负责在底层数据存储中存储或读取数据。大多数时候,你会使用SQLite作为持[2]久化存储,它依赖于广泛使用的SQLite数据库,在磁盘上存储数据。Core Data也提供其他存储类型(比如XML、二进制数据、内存)的选项,但是现在我们不需要考虑其他的存储类型。1.2 数据建模

Core Data存储结构化的数据。所以为了使用Core Data,我们首先需要创建一个数据模型(或者是大纲(schema),如果你乐意这么叫它)来描述我们的数据结构。

你可以通过代码来定义一个数据模型。但是使用 Xcode 的模型编辑器创建和编辑.xcdatamodeld文件会更容易。在你开始用Xcode模板创建新的iOS或OS X应用程序时,可以在File > New弹出的菜单里的Core Data部分中选择“Data Model”来创建一个数据模型。如果你在第一次创建项目时勾选了“Use Core Data”这个选项,那么Xcode将为你创建一个空的数据模型。

事实上,你并不需要通过勾选“Use Core Data”选项来使用Core Data——相反,我们建议你不要这么做,因为我们之后会把生成的模板代码都删掉。

如果你在Xcode的project navigator里选中了数据模型文件,Xcode的数据模型编辑器就会打开,我们就可以开始工作了。实体和属性

实体(entity)是数据模型的基石。正因为如此,一个实体应该代表你的应用程序里有意义的一部分数据。例如,在我们的例子里,我们创建了一个叫Mood的实体,它有两个属性:一个代表颜色,一个代表拍摄照片的日期。按照惯例,实体名称以大写字母开头,这和类的名称的命名方式类似。

Core Data自身就支持很多数据类型:数值类型(整数和不同大小的浮点数,以及十进制数值)、字符串、布尔值、日期、二进制数据,以及存储着实现了NSCoding协议的对象或者是提供了自定义值转换器(value transformer)的对象的可转换类型。

对于Mood实体,我们创建了两个属性:一个是日期类型(被称为date),另一个是可转换类型(被称为colors)。属性的名称应该以小写字母开头,就像类或者结构体里的属性一样。colors属性是一个数组,里面都是UIColor对象,因为NSArray和UIColor已经遵循了NSCoding协议,所以我们可以把这样的数组直接存入一个可转换类型的属性里,如图1.3所示。图1.3 在Xcode模型编辑器里的Mood实体

属性选项

两个属性都有更多的一些选项可以让我们调整。我们把date属性标记为必选的(non-optional)和可索引的(indexed)。colors数组也标记为必选属性。

必选属性必须要赋给它们恰当的值,才能保存这些数据。把一个属性标记为可索引时,Core Data会在底层SQLite数据库表里创建一个索引。索引可以加速这个属性的搜索和排序,但代价是插入数据时性能下降和需要额外的存储空间。在我们的例子里,我们会以mood对象的时间来排序,所以把date属性标记为可索引是有意义的如图1.4所示。本书会在第6章中深入探讨这个主题。图1.4 Mood实体的属性托管对象子类

现在我们已经创建好了数据模型,我们需要创建代表Mood实体的托管对象子类。实体只是描述了哪些数据属于mood对象。为了在代码中能使用这个数据,我们需要一个具有和实体里定义的属性们相对应的属性的类。

一个好的实践是按它们所代表的东西来命名这些类,并且不用添加类似Entity这样的后缀。比如,我们的类直接叫Mood而不是MoodEntity。实体和类都叫Mood,非常完美。

对于创建类,建议不要使用Xcode的代码生成工具(Editor > Create NSManagedObject Subclass...),而是直接手写它们。到最后,你会发现,每次只需要写很少几行代码,就能完全掌控它们的好处。此外,手写代码还会让整个流程变得更加清楚,你会发现其中并没有什么魔法。

我们的Mood实体在代码里是像这样的:

public final class Mood: ManagedObject {

@NSManaged public private(set) var date: NSDate

@NSManaged public private(set) var colors: [UIColor]

}

它的父类ManagedObject只是一个继承至NSManagedObject的空的子类:

public class ManagedObject: NSManagedObject {

}

我们需要ManagedObject这个父类的唯一原因是要满足Swift中的泛型类型约束的工作方式。我们在后面遇到相关内容的时候还会提到这个问题,现在你可以简单地认为它和NSManagedObject是等价的。

修饰Mood类属性的@NSManaged标签告诉编译器这些属性将由Core Data来实现。Core Data用一种很不同的方式来实现它们,在本书第二部分里会详细谈论这部分内容。private(set)这个访问控制修饰符表示这两个属性都是公开只读的。Core Data其实并不强制执行这样的只读策略,但我们在类中定义了这些标记,于是编译器将保证它们是公开只读的。

在我们的例子里,没有必要将之前提到的属性标记为公开可写。我们会创建一个辅助方法来插入以特定值创建的新的mood对象,之后我们就再也不会修改这些值了。所以一般而言,最好的做法是,只有当你真正需要的时候,才把对象里的属性和方法公开地暴露出来。为了能让Core Data识别我们的Mood类,并把它和Mood实体相关联,我们在模型编辑器里选中这个实体,然后在data model inspector里输入它的类名。因为我们用了Swift的模组(module),所以我们还需要选中这个定义的模组。1.3 设置Core Data栈

现在我们有第一个版本的数据模型和Mood类了,可以开始设置一个基本的Core Data栈了。我们暴露了如下的方法来创建主托管对象上下文。我们会在整个App里都使用这个上下文:

private let StoreURL = NSURL.documentsURL

.URLByAppendingPathComponent("Moody.moody")

public func createMoodyMainContext() -> NSManagedObjectContext {

let bundles = [NSBundle(forClass: Mood.self)]

guard let model = NSManagedObjectModel

.mergedModelFromBundles(bundles)

else { fatalError("model not found") }

let psc = NSPersistentStoreCoordinator(managedObjectModel: model)

try! psc.addPersistentStoreWithType(NSSQLiteStoreType,configuration: nil,

URL: StoreURL,options: nil)

let context = NSManagedObjectContext(

concurrencyType:.MainQueueConcurrencyType)

context.persistentStoreCoordinator = psc

return context

}

让我们一步步地分析上面的代码。

首先,我们获取了托管对象模型所在的bundle。这里我们调用了NSBundle(forClass:)方法,这样一来,就算我们把代码移动到了另一个模组里,它也同样能够工作。然后我们调用了NSManagedObjectModel的辅助方法mergedModelFromBundles(_:) 来加载数据模型。这个方法会搜索指定bundle里的模型,并将它们合并成一个托管对象模型。由于这里只有一个模型,所以它只会简单加载那一个。

接下来,我们创建了持久化存储协调器。在用对象模型初始化它之后,我们给它添加了类型为NSSQLiteStoreType的持久化存储。存储的位置是由私有的StoreURL常量指定的,它指向documents目录里的Moody.moody文件。如果数据库已经存在于这个路径,那么它会被打开;否则,Core Data会在这个位置创建一个新的数据库。

addPersistentStoreWithType(_:configuration:URL:options:)方法可能会抛出错误,所以我们需要显式地处理它。或者可以使用try!关键词,如果发生错误,那么这会导致一个运行时错误。在我们的例子里,我们使用了try!关键词,因为并没有什么可行的方法能从这种错误中恢复。

最后,我们使用.MainQueueConcurrencyType选项创建了托管对象上下文,并把协调器赋值给这个上下文的persistentStoreCoordinator属性。.MainQueueConcurrencyType表示这个上下文是绑定到主线程的,也就是我们处理所有UI交互的地方。我们可以从UI代码的任何地方安全地访问这个上下文和其中的托管对象。我们会在第8章中介绍更多关于这部分的内容。

因为我们把所有的模板代码都封装到了一个简洁的辅助方法里,我们可以在应用程序代理(application delegate)里通过一个简单的createMoodyMainContext()方法调用来初始化主上下文:

class AppDelegate: UIResponder,UIApplicationDelegate {

let managedObjectContext = createMoodyMainContext()

//...

}1.4 显示数据

现在我们已经初始化好Core Data栈了,接下来我们可以使用在应用程序代理里创建的托管对象上下文来查询我们需要显示的数据了。

为了方便在view controller里使用这个托管对象上下文,我们在应用程序代理里把这个上下文对象传递给第一个view controller,然后通过它再传递给视图层次里的其他view controller。我们通过定义一个协议来让这种组织方式表现得更明显:

protocol ManagedObjectContextSettable: class {

var managedObjectContext: NSManagedObjectContext! { get set }

}

现在我们让视图层次里的第一个view controller实现这个协议:

class RootViewController: UIViewController,ManagedObjectContextSettable {

var managedObjectContext: NSManagedObjectContext!

//...

}

最后,我们可以在应用程序代理给实现了这个协议的root view controller设置上下文对象:

func application(application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)

-> Bool

{

//...

guard let vc = window?.rootViewController

as? ManagedObjectContextSettable

else { fatalError("Wrong view controller type") }

vc.managedObjectContext = managedObjectContext

//...

}

与此类似,我们把托管对象上下文从root view controller传递给了实际需要这个上下文来展示数据的table view controller。因为我们的示例项目使用了Storyboard,我们可以通过挂钩(hook)view controller的prepareForSegue(_:sender:)的方法来实现这个需求:

override func prepareForSegue(segue: UIStoryboardSegue,

sender: AnyObject?)

{

switch segueIdentifierForSegue(segue) {

case.EmbedNavigation:

guard let nc = segue.destinationViewController

as? UINavigationController,

let vc = nc.viewControllers.first

as? ManagedObjectContextSettable

else { fatalError("wrong view controller type") }

vc.managedObjectContext = managedObjectContext

}

}

这个模式和我们在应用程序代理里做的非常类似,不同的是现在我们需要先遍历navigation controller来拿到MoodsTableViewController实例,它遵从了ManagedObjectContextSettable协议。

如果你对segueIdentifierForSegue(_:)这个方法的由来感到好奇,[3]可以参考WWDC 2015的Swift in Practice这个session,我们参考了里面的这个模式。这是在Swift里使用协议扩展(protocol extension)的绝好例子,它让segue变得更加显式,还可以让编译器检查我们是否处理了所有的情况。

为了展示mood对象——虽然我们现在还没有数据,我们可以先剧透一点——我们会使用table view与Core Data的NSFetchedResultsController的组合来显示数据。这个类会监听我们数据集的变化,然后以一种非常容易就可以更新对应的table view的方式来通知我们这些变化。获取请求

顾名思义,一个获取(Fetch)请求描述了哪些数据需要被从持久化存储里取回,以及它们是如何被取回的。我们会使用获取请求来取回所有的Mood实例,并把它们按照创建时间进行排序。获取请求还可以设置非常复杂的过滤条件,并只取回一些特定的对象。事实上,由于获取请求如此复杂,后面会再详细讨论这些内容。

需要指出的重要一点是:每次你执行一个获取请求,Core Data会穿过整个Core Data栈,直到文件系统。按照API约定,获取请求就是往返的:从上下文,经过持久化存储协调器和持久化存储,降入SQLite,然后原路返回。

虽然获取请求是强有力的工具,但是它们需要做很多的工作。执行一个获取请求是一个相对昂贵的操作。我们会在第二部分里详细讨论具体原因以及如何避免掉这些开销。现在,我们只要记住,要慎重地使用获取请求,因为它们可能是一个潜在的性能瓶颈。通常,我们可以通过遍历关系来避免使用获取请求,本书后面还会提到这些内容。

再回到我们的例子里。这里演示了我们如何创建一个获取请求来从Core Data里取回所有的Mood实例,并按它们的创建时间降序排列(我们很快会整理这部分代码):

let request = NSFetchRequest(entityName: "Mood")

let sortDescriptor = NSSortDescriptor(key: "date",ascending: false)

request.sortDescriptors = [sortDescriptor]

request.fetchBatchSize = 20

这个entityName参数是我们的Mood实体在数据模型里的名称。而fetchBatchSize属性告诉Core Data一次只获取特定的数量的mood对象。这背后其实发生了许多“魔法”;后面会在第4章里深入了解这些机制。我们设置的批次大小为20,这大约也是屏幕能显示项数的两倍。我们会在性能这一章节里继续探讨如何调整批次大小的问题。

简化模型类

在继续开始使用这个获取请求之前,我们会先给模型类添加一些方法,让之后的代码变得更容易使用和维护。

我们会演示一种创建获取请求的方式,它能更好地将关注点进行分离(separation of concerns,SoC)。之后我们在扩展示例程序其他方面的时候这个模式也能派上用场。[4]

译者注:关注点分离,是面向对象的程序设计的核心概念。分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用(将针对特定领域问题代码抽象化成较少的程式码,例如将代码封装成类或是函数),业务逻辑同特定领域问题的关系被封装,易于维护,这样原本分散在整个应用程序中的变动就可以很好地被管理起来。

在Swift中,协议扮演了核心角色。我们会给Mood模型添加并实现一个协议。事实上,我们后面添加的模型类都会实现这个协议——我们建议在你的模型类里也这么做:

public protocol ManagedObjectType: class {

static var entityName: String { get }

static var defaultSortDescriptors: [NSSortDescriptor]{ get }

}[5]

我们利用Swift的协议扩展来为这个协议添加一个默认的实现,为defaultSortDescriptors属性返回一个空数组。另外,我们还会添加一个计算属性(computed property)用来返回一个使用默认排序描述符的获取请求。

extension ManagedObjectType {

public static var defaultSortDescriptors: [NSSortDescriptor]{

return []

}

public static var sortedFetchRequest: NSFetchRequest {

let request = NSFetchRequest(entityName: entityName)

request.sortDescriptors = defaultSortDescriptors

return request

}

}

现在我们让Mood类遵循这个协议。我们实现了静态的entityName属性并且添加了自定义的默认排序描述符。我们希望Mood的实例默认按日期排序(就像我们之前创建的获取请求里做的那样):

extension Mood: ManagedObjectType {

public static var entityName: String {

return "Mood"

}

public static var defaultSortDescriptors: [NSSortDescriptor]{

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

}

}

通过这个扩展,我们可以像这样来创建和上面相同的获取请求:

let request = Mood.sortedFetchRequest

request.fetchBatchSize = 20

我们在后面会以这个模式为基础,给ManagedObjectType协议添加更多的便利方法——比如,创建获取请求的时候指定谓词[6](predicate)或者是搜索这个类型的对象。你可以参考示例代码里的ManagedObjectType协议的所有扩展方法和属性。

通过使用ManagedObjectType协议,我们把实体的名称封装到了它对应的模型类的扩展里,然后我们给Mood类添加了一个方便的方法来获取预先配置好的获取请求。

现在,我们看上去似乎做了很多不必要的工作。但这其实是一种非常干净的设计,也是一个值得依赖的良好基础。随着我们的App变得越来越复杂,我们会更多地使用这个模式。我们不需要在用到Mood类的地方写死这些信息。我们改善了关注点分离。通过这些改动,Mood类将知道它的实体和实体的默认排序方式是什么。Fetched Results Controller

我们使用NSFetchedResultsController类来协调模型和视图。在我们的例子里,我们用它来让table view和Core Data中的mood对象保持一致。fetched results controller还可以用于其他场景,比如在使用collection view的时候。

使用fetched results controllers的主要优势是:我们不是直接执行获取请求然后把结果交给table view,而是在当底层数据有变化时,它能通知我们,让我们很容易地更新table view。为了做到这一点,fetched results controllers监听了一个通知,这个通知会由托管对象上下文在它之中的数据发生改变的时候所发出(第5章中会更多有关于这方面的内容)。fetched results controllers会根据底层获取请求的排序,计算出哪些对象的位置发生了变化,哪些对象是新插入的等,然后把这些改动报告给它的代理,如图1.5所示。图1.5 fetched results controller与table view是如何交互的

为了初始化mood table view的fetched results controller,我们在UITableViewController子类的viewDidLoad()方法里调用了setupTableView()这个方法。setupTableView()使用了前面提到的获取请求来创建一个fetched results controller,接着我们把它传给了一个自定义类,这个类封装了所有fetched results controller的代理所需要的模板代码。

private func setupTableView() {

//...

let request = Mood.sortedFetchRequest

request.returnsObjectsAsFaults = false

request.fetchBatchSize = 20

let frc = NSFetchedResultsController(fetchRequest: request,

managedObjectContext: managedObjectContext,

sectionNameKeyPath: nil,cacheName: nil)

let dataProvider = FetchedResultsDataProvider(

fetchedResultsController: frc,delegate: self)

//...

}

FetchedResultsDataProvider类实现了fetched results controller如下的三个代理方法,它们会在底层数据发生变化的时候通知我们:

1.controllerWillChangeContent(_:)

2.controller(_:didChangeObject:...)

3.controllerDidChangeContent(_:)

我们可以在view controller的类里直接实现上面的这些方法。但是这样的模板代码会把view controller弄得很乱,因为我们可能随时需要使用fetched results controller。所以我们打算从一开始就把这些代理方法的实现写在可以复用的FetchedResultsDataProvider类里:

class FetchedResultsDataProvider: NSObject,

NSFetchedResultsControllerDelegate,DataProvider

{

//...

init(fetchedResultsController: NSFetchedResultsController,

delegate: Delegate)

{

self.fetchedResultsController = fetchedResultsController

self.delegate = delegate

super.init()

fetchedResultsController.delegate = self

try! fetchedResultsController.performFetch()

}

func controllerWillChangeContent(controller: NSFetchedResultsController) {

//...

}

func controller(controller: NSFetchedResultsController,

didChangeObject anObject: AnyObject,

atIndexPath indexPath: NSIndexPath?,

forChangeType type: NSFetchedResultsChangeType,

newIndexPath: NSIndexPath?)

{

//...

}

func controllerDidChangeContent(controller: NSFetchedResultsController) {

delegate.dataProviderDidUpdate(updates)

}

}

在初始化的时候,FetchedResultsDataProvider把自己设置成了fetched results controller的代理。然后它调用了performFetch(_:),方法从持久化存储中加载了这些数据。由于这个方法可能会抛出错误,我们在它前面加了try!关键词来让它尽早崩溃,因为这是一个编程上的错误。

在这些代理方法里,data provider类把fetched results controller报告的改动聚合到了一个叫DataProviderUpdate的枚举实例的数组里:

enum DataProviderUpdate {

case Insert(NSIndexPath)

case Update(NSIndexPath,Object)

case Move(NSIndexPath,NSIndexPath)

case Delete(NSIndexPath)

}

在更新周期的最后(也就是controllerDidChangeContent(_:) 方法里),data provider会把这些更新转交给它的代理。

我们之后可以在其他table view,甚至collection view里复用这个[7]类。具体请参考示例项目里的完整源代码。

当fetched results controller和它的代理都就位后,我们就可以继续下一步了:让table view里实际地显示出数据。为此,我们需要实现table view的数据源(data source)方法。我们遵循类似处理fetched results controller代理方法的原则,把数据源方法都封装到一个单独可复用的类里。这里显示了我们是如何让fetched results controller代理和table view数据源以及其他部分交互的,如图1.6所示。

和data provider类似,我们在setupTableView()方法里初始化了数据源实例,并把之前创建的data provider作为参数传了进去。图1.6 data provider和数据源类封装了让table view与fetched results controller保持更新的模板代码

private func setupTableView() {

//...

dataSource = TableViewDataSource(tableView: tableView,

dataProvider: dataProvider,delegate: self)

}

这样一来,数据源对象就可以使用data provider来获取实现table view数据源方法所需要的信息了:

func tableView(tableView: UITableView,numberOfRowsInSection section: Int)

-> Int

{

return dataProvider.numberOfItemsInSection(section)

}

func tableView(tableView: UITableView,

cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell

{

let object = dataProvider.objectAtIndexPath(indexPath)

let identifier = delegate.cellIdentifierForObject(object)

guard let cell = tableView.dequeueReusableCellWithIdentifier(

identifier,forIndexPath: indexPath) as? Cell

else { fatalError("Unexpected cell type at \(indexPath)") }

cell.configureForObject(object)

return cell

}

数据源还暴露了一个processUpdates(_:)方法,让我们可以把从fetched results data provider里接收到的更新传参进去。这里更多的是关于UIKit的细节而非Core Data,所以我们只会简单地描述一下。当[8]然,你可以阅读示例工程里的完整源代码。

最后一步是把这些部分串起来,实现data provider和数据源的代理方法:

extension MoodsTableViewController: DataProviderDelegate {

func dataProviderDidUpdate(updates: [DataProviderUpdate]?) {

dataSource.processUpdates(updates)

}

}

extension MoodsTableViewController: DataSourceDelegate {

func cellIdentifierForObject(object: Mood) -> String {

return "MoodCell"

}

}

第一个方法只是把data provider的更新传给了table view的数据源。第二个方法直接返回了cell的标识符。

我们同样需要让我们的cell类遵循ConfigurableCell协议:

protocol ConfigurableCell {

typealias DataSource

func configureForObject(object: DataSource)

}

这是我们的table view数据源的一个需求,它让我们可以调用cell的configureForObject(_:)方法,并使用底层数据来合理地配置这个cell。所以MoodTableViewCell的实现就很直接了:

extension MoodTableViewCell: ConfigurableCell {

func configureForObject(mood: Mood) {

moodView.colors = mood.colors

label.text = sharedDateFormatter.stringFromDate(mood.date)

country.text = mood.country?.localizedDescription ?? ""

}

}

我们已经走了很远了。我们创建了模型,设置了Core Data栈,在view controller层级里传递托管对象上下文,我们创建了获取请求,然后用fetched results controller来让table view展示数据。现在唯一缺失的部分是显示所需的实际数据,让我们开始讨论它吧!1.5 操作数据

如同本章最开始概述的那样,所有的被Core Data托管的对象,比如我们的Mood类的实例,都存在于一个托管对象上下文里。所以,插入的新对象和删除已有对象同样是在上下文里完成的。你可以把托管对象上下文当成一个暂存器(scratchpad):你在上下文里改动的对象都不会被持久化,除非你显式地调用上下文的save()方法来保存它们。插入对象

在我们的示例App里,插入新的mood对象是通过拍摄新的照片完成的。这里我们不会包含所有非Core Data的代码,其他的代码可[9]以参考在GitHub上的源代码。

当用户拍摄新照片的时候,我们通过调用在NSEntityDescription上的insertNewObject-ForEntityName(_:inManagedObjectContext:)方法来插入一个新的mood对象,并把图片的最主要的颜色赋值给它,最后调用上下文的save()方法:

guard let mood = NSEntityDescription.insertNewObjectForEntityForName(

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载