在本章中,我们将创建一个简单的使用Core Data的示例程序。在这个过程中,我们会介绍Core Data的基本架构以及在此场景下如何正确使用它。当然,本章提到的方方面面都有更多值得一谈的内容。不过请放心,后面我们会详细回顾这些内容。
本章会介绍这个示例程序中与Core Data相关的所有方面的内容。请注意这并不是一个从头开始一步步教你如何创建整个应用的教程。我们推荐你看一下在GitHub上完整的代码 来了解在实际项目中不同的部分。
这个示例应用程序包括一个简单的table view和底部的实时摄像头拍摄的内容。拍摄一张照片后,我们从照片中提取出它的一组主色。然后存储这些配色方案(我们称其为“mood”),并相应地更新table view,如图1.1所示。
为了更好地理解Core Data的架构,在我们开始创建这个示例应用之前,让我们先来看一看它的主要组成部分。在本书第二部分中会详细介绍所有这些部分是如何协同工作的。
一个基本的Core Data栈由四个主要部分组成:托管对象(managed objects)(NSManaged Object)、托管对象上下文(managed object context)(NSManaged Object Context)、持久化存储协调器(persistent store coordinator)(NSPersistent Store Coordinator),以及持久化存储(persistent store)(NSPersistent Store),如图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作为持久化存储,它依赖于广泛使用的SQLite数据库 ,在磁盘上存储数据。Core Data也提供其他存储类型(比如XML、二进制数据、内存)的选项,但是现在我们不需要考虑其他的存储类型。
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),所以我们还需要选中这个定义的模组。
现在我们有第一个版本的数据模型和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()
//...
}
现在我们已经初始化好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(_:)这个方法的由来感到好奇,可以参考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)。之后我们在扩展示例程序其他方面的时候这个模式也能派上用场。
译者注:关注点分离 ,是面向对象的程序设计的核心概念。分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用(将针对特定领域问题代码抽象化成较少的程式码,例如将代码封装成类或是函数),业务逻辑同特定领域问题的关系被封装,易于维护,这样原本分散在整个应用程序中的变动就可以很好地被管理起来。
在Swift中,协议扮演了核心角色。我们会给Mood模型添加并实现一个协议。事实上,我们后面添加的模型类都会实现这个协议——我们建议在你的模型类里也这么做:
public protocol ManagedObjectType: class {
static var entityName: String { get }
static var defaultSortDescriptors: [NSSortDescriptor]{ get }
}
我们利用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协议添加更多的便利方法——比如,创建获取请求的时候指定谓词(predicate)或者是搜索这个类型的对象。你可以参考示例代码 里的ManagedObjectType协议的所有扩展方法和属性。
通过使用ManagedObjectType协议,我们把实体的名称封装到了它对应的模型类的扩展里,然后我们给Mood类添加了一个方便的方法来获取预先配置好的获取请求。
现在,我们看上去似乎做了很多不必要的工作。但这其实是一种非常干净的设计,也是一个值得依赖的良好基础。随着我们的App变得越来越复杂,我们会更多地使用这个模式。我们不需要在用到Mood类的地方写死这些信息。我们改善了关注点分离。通过这些改动,Mood类将知道它的实体和实体的默认排序方式是什么。
我们使用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<Delegate: DataProviderDelegate>: 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<Object> {
case Insert(NSIndexPath)
case Update(NSIndexPath,Object)
case Move(NSIndexPath,NSIndexPath)
case Delete(NSIndexPath)
}
在更新周期的最后(也就是controllerDidChangeContent(_:) 方法里),data provider会把这些更新转交给它的代理。
我们之后可以在其他table view,甚至collection view里复用这个类。具体请参考示例项目里的完整源代码 。
当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,所以我们只会简单地描述一下。当然,你可以阅读示例工程里的完整源代码 。
最后一步是把这些部分串起来,实现data provider和数据源的代理方法:
extension MoodsTableViewController: DataProviderDelegate {
func dataProviderDidUpdate(updates: [DataProviderUpdate<Mood>]?) {
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展示数据。现在唯一缺失的部分是显示所需的实际数据,让我们开始讨论它吧!
如同本章最开始概述的那样,所有的被Core Data托管的对象,比如我们的Mood类的实例,都存在于一个托管对象上下文里。所以,插入的新对象和删除已有对象同样是在上下文里完成的。你可以把托管对象上下文当成一个暂存器(scratchpad):你在上下文里改动的对象都不会被持久化,除非你显式地调用上下文的save()方法来保存它们。
在我们的示例App里,插入新的mood对象是通过拍摄新的照片完成的。这里我们不会包含所有非Core Data的代码,其他的代码可以参考在GitHub 上的源代码。
当用户拍摄新照片的时候,我们通过调用在NSEntityDescription上的insertNewObject-ForEntityName(_:inManagedObjectContext:)方法来插入一个新的mood对象,并把图片的最主要的颜色赋值给它,最后调用上下文的save()方法:
guard let mood = NSEntityDescription.insertNewObjectForEntityForName(
"Mood",inManagedObjectContext: moc) as? Mood
else { fatalError("Wrong object type") }
mood.colors = image.moodColors
try! moc.save()
但是,我们写了这么多笨重的代码只是为了插入一个对象。首先,我们需要把插入调用返回的结果向下转换成Mood类型。然后,我们希望colors是公开只读的。最后,我们其实应该要去处理save()可能抛出的错误。
我们会介绍一些辅助方法来整理这些代码。最后的结果会让我们的代码变得更简单。首先,我们给NSManagedObjectContext添加一个方法,让获取新插入对象时不再需要每次手动做向下的类型转换,也不需要通过实体名称来指代它的类型。我们利用在ManagedObjectType协议中引入的静态entityName属性来实现这个功能:
extension NSManagedObjectContext {
public func insertObject<A: ManagedObject where A: ManagedObjectType>
() -> A
{
guard let obj = NSEntityDescription.insertNewObjectForEntityForName(
A.entityName,inManagedObjectContext: self) as? A
else { fatalError("Wrong object type") }
return obj
}
}
这个方法通过A来定义了一个泛型方法,A是遵从ManagedObjectType协议的ManagedObject子类型。编译器会从方法的类型注解(type annotation)自动推断出我们尝试插入的对象类型:
let mood: Mood = moc.insertObject()
接下来,我们在给Mood类添加的静态方法里使用这个新的辅助方法来封装对象的插入:
public final class Mood: ManagedObject {
//...
public static func insertIntoContext(moc: NSManagedObjectContext,
image: UIImage) -> Mood
{
let mood: Mood = moc.insertObject()
mood.colors = image.moodColors
mood.date = NSDate()
return mood
}
//...
}
最后,我们给上下文添加两个帮助保存的辅助方法:
extension NSManagedObjectContext {
public func saveOrRollback() -> Bool {
do {
try save()
return true
} catch {
rollback()
return false
}
}
public func performChanges(block: () -> ()) {
performBlock {
block()
self.saveOrRollback()
}
}
}
第一个方法 saveOrRollback(),直接捕获了调用save()方法可能抛出的异常,并在出错的时候回滚挂起的改动。也就是说,它直接扔掉了那些没有保存的数据。对于我们的示例App而言,这是一种可以接受的行为,因为在我们的设置里,单个托管对象上下文是不会出现保存冲突的。然而具体到你能否这么做,还是取决于你使用Core Data的方式,也许你需要更精密的处理。第5章和第9章这两章中都会有关于如何解决保存冲突的更深入的内容。第二个方法 performChanges(_:),调用了上下文的performBlock(_:) 方法,它将执行作为参数传入的block,然后保存上下文。调用performBlock(_:)方法能确保我们是从正确的队列里访问上下文和它的托管对象。当我们需要添加第二个在后台队列里的上下文时,这就显得很重要了。现在,你只需要把这种做法当成一个最佳实践模式即可:始终把和Core Data对象交互的代码封装在类似的一个block里。
现在,每当用户拍摄了一张新的照片,我们只需要在root view controller写三行代码,就能插入一个新的mood对象了:
func didTakeImage(image: UIImage) {
self.managedObjectContext.performChanges {
Mood.insertIntoContext(self.managedObjectContext,image: image)
}
}
在整个项目里,我们可以复用这些辅助方法来编写更干净、可读的代码——这并不需要引入什么魔法。另外,我们已经打下了一个良好的使用最佳实践模式基础,可以帮助我们应对程序变得越来越复杂的情况。
为了演示删除对象的最佳实践,我们会添加一个detail view controller,它会显示关于单个mood的信息,并且允许用户删除特定的mood。接下来我们将对这个示例应用进行扩展,使得你在table view中选择一个mood时,detail view controller可以被推入导航栈中。
当指向这个detail view controller的segue触发时,我们把选中的mood对象设置为这个新创建的view controller的一个属性值:
override func prepareForSegue(segue: UIStoryboardSegue,sender: AnyObject?) {
switch segueIdentifierForSegue(segue) {
case.ShowMoodDetail:
guard let vc = segue.destinationViewController
as? MoodDetailViewController
else { fatalError("Wrong view controller type") }
guard let mood = dataSource.selectedObject
else { fatalError("Showing detail,but no selected row?") }
vc.mood = mood
}
}
这个view controller还有一个删除按钮用来删除你当前看到的mood,它最终会触发如下操作:
@IBAction func deleteMood(sender: UIBarButtonItem) {
mood.managedObjectContext?.performChanges {
self.mood.managedObjectContext?.deleteObject(self.mood)
}
}
为了能让删除生效,我们调用之前介绍的mood对象的上下文的performChanges(_:)辅助方法。接着我们在block里,调用了deleteObject(_:)方法,并把mood对象作为参数传递了进去,最后performChanges(_:)这个辅助方法执行完删除操作后会保存上下文。
很自然,如果mood对象被删除了,让detail view controller还在栈里并没有什么意义。最直接的做法是,在我们删除mood对象的同时弹出这个detail view controller。不过,我们将要采取一种更泛用的方法。这种方法同样能应对mood对象可能在后台网络同步操作时被删除的情景。
我们要使用的方法和fetched results controller中的方式一样:监听“ 对象已改变 ”(objects-did-change)通知。托管对象上下文发出这些通知来告知你托管对象的变化。使用这种方式,无论这个改变的来源是什么,最后达到的效果是一致的。
为了达到这个目的,我们构建了一个托管对象观察者(managed object observer),它接受两个参数,一个参数是被观察的对象,另一个参数是一个会在这个对象被删除或者改动的时候被调用的闭包:
public final class ManagedObjectObserver {
public init?(object: ManagedObjectType,changeHandler: ChangeType -> ()) {
//...
}
}
在我们的detail view controller里,可以这样来初始化这个观察者:
private var observer: ManagedObjectObserver?
var mood: Mood! {
didSet {
observer = ManagedObjectObserver(object: mood) { [unowned self]type in
guard type ==.Delete else { return }
self.navigationController?.popViewControllerAnimated(true)
}
updateViews()
}
}
我们在mood这个属性的didSet属性观察方法里初始化了一个观察者对象,并将它作为一个实例变量保存。当被观察的mood对象被删除的时候,这个闭包会以.Delete作为变化类型参数被调用,最后我们从导航栈里弹出这个detail view controller。这是一种更健壮、更通用的解决方案,因为无论这个删除操作是由用户直接触发的,或者是在后台通过网络触发的,我们都会收到对象被删除的通知。
ManagedObjectObserver类注册了“ 对象已改变 ”的通知(NSManagedObjectContextObjects-DidChangeNotification)——Core Data在每次上下文里的托管对象发生变化的时候都会发出这个通知。它为我们感兴趣的托管对象所在的上下文注册了这个通知,当收到通知之后,它会遍历通知的user info字典来检查被观察的对象是否被删除:
public final class ManagedObjectObserver {
public enum ChangeType {
case Delete
case Update
}
public init?(object: ManagedObjectType,changeHandler: ChangeType -> ()) {
guard let moc = object.managedObjectContext else { return nil }
objectHasBeenDeleted = !object.dynamicType.defaultPredicate
.evaluateWithObject(object)
token = moc.addObjectsDidChangeNotificationObserver {
[unowned self]note in
guard let changeType = self.changeTypeOfObject(object,
inNotification: note)
else { return }
self.objectHasBeenDeleted = changeType ==.Delete
changeHandler(changeType)
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(token)
}
private var token: NSObjectProtocol!
private var objectHasBeenDeleted: Bool = false
private func changeTypeOfObject(object: ManagedObjectType,
inNotification note: ObjectsDidChangeNotification) -> ChangeType?
{
let deleted = note.deletedObjects.union(note.invalidatedObjects)
if note.invalidatedAllObjects ||
deleted.containsObjectIdenticalTo(object)
{
return.Delete
}
let updated = note.updatedObjects.union(note.refreshedObjects)
if updated.containsObjectIdenticalTo(object) {
return.Update
}
return nil
}
}
只要托管对象上下文发送了 已经改变 的通知,我们就检查是否所有对象都已经被无效化,或者被观察的对象需要被删除或者被无效化。这两种情况我们都会调用changeHandler(_:)这个闭包,并传入变化类型为.Delete的值作为参数。类似地,如果对象需要被更新或者重新加载,我们也会调用这个闭包,并传入改变类型为.Update的值作为参数。
在观察者的代码里有两件有趣的事情值得注意:首先,为了观察上下文的通知,我们把NSNotification的user info字典里的松散的类型信息的数据做了强类型封装。这样能让代码更安全、更易读,同时也把所有的类型转换都封装到了一个地方。你可以查看GitHub上的完整代码 来深入了解这个封装。
其次,containsObjectIdenticalTo(_:)方法使用了相同指针地址的比较方法(===)来比较被观察对象集合里的对象。我们可以这样做的原因是,Core Data保证对象的 唯一性 :Core Data保证对于任意的持久化存储条目,在一个托管对象上下文里只会存在一个单独的托管对象。在本书第二部分中会介绍更多的细节内容。
本章已经涵盖了很多基础内容。我们创建了一个虽然简单,但实际可用的示例应用程序。最初,我们定义了数据结构,这是通过使用一个实体和其属性来创建数据模型所实现的。然后我们为这个实体创建了对应的NSManagedObject子类。为了设置Core Data栈,我们加载了之前定义的数据模型,创建了一个持久化存储协调器,并给它添加了一个SQL存储。最后,我们创建了托管对象上下文并把持久化存储协调器设置为它的一个属性。
Core Data栈设置好后,我们使用了fetched results controller从存储里加载mood对象并用table view来展示。我们还增加了插入和删除mood对象的功能。我们使用了响应式(reactive)的方法在数据发生变化的时候来更新我们的UI:对于table view,我们使用了Core Data的fetched results controller;对于detail view,我们使用了自己实现的基于上下文变更通知的托管对象观察者。
·Core Data不仅仅能用来完成复杂的持久化任务,它在像本章所展示的这个简单的项目里,也可以工作得很好。
·你并不需要代码生成器来创建托管对象的子类们;手写它们其实很容易,还能让事情完全被掌控。
·可以使用协议来扩展你的模型类,比如添加一个实体名称、默认排序描述符或是与它相关信息的协议,这样可以避免它们散落在代码的各个地方。
·把数据源和fetched results controller的代理方法封装到分离的类里,这样有助于代码复用,保持view controller精简,也更符合Swift的类型安全特性。
·通过创建一些简单的辅助方法,能在插入对象、执行获取请求或是执行类似重复的任务的时候让你的生活变得轻松一点。
·当前展示的对象被删除或者改变的时候,确保你的UI能被更新。我们推荐使用响应式编程来处理这类任务:fetched results controller已经为table views做了这些处理。你可以通过观察上下文的“已经改变”的通知来实现类似的模式。