以前使用 Core Data 时,总要写一堆繁琐的样板代码,导致大家学习 Core Data 也提不起什么兴趣,iOS 10 苹果对 Core Data 的使用体验做出了一些改进,节省了开发者很多时间,主要体现在以下两点:
managed object context
和 persistent store coordinator
也提供了一些有用的新特性,可能会影响你构建代码的方式。
本章我们构建一个马铃薯的评分 App --- TaterRater 运行下先来大概看一下效果:
大体结构如下,一个 split view controller 展示列表和详情页面,Model 目录下存在一个土豆的模型类 Potato.swift
既然本章的内容是 Core Data,那就先来创建一个 Data Model ,命名为 TaterRater.xcdatamodeld
然后创建一个 Potato 实体,并添加如下属性
现在就不需要 Potato.swift 文件了,删除它。选中 Potato 实体,在右侧的面板中可以看到在 Class 这一栏多了 Codegen 这一栏,可以用来决定代码的组织形式:
此时我们选择 Class Definition
试着编译一下,编译没有问题(但是运行会崩溃)。虽然我们删除了 Potato.swift 文件,也没有创建新的 model 文件,为什么编译不会报错?这是因为 Xcdoe 自动替我们生成了 model 的子类,它放置于系统的 Derived Data 文件夹下(和工程相关的目录中),以后每当在模板 *.xcdatamodeld 上的修改都会自动映射到相关的模型类上,不再需要我们手动去重新生成模型类了。
你可以打开 AppDelegate.swift 找到 Potato 类型,手动 Command + 鼠标左键跳转到它定义的位置,查看系统提供的实现细节:
import Foundation import CoreData extension Potato { @nonobjc public class func fetchRequest() -> NSFetchRequest<Potato> { return NSFetchRequest<Potato>(entityName: "Potato"); } @NSManaged public var crowdRating: Float @NSManaged public var notes: String? @NSManaged public var userRating: Int16 @NSManaged public var variety: String? }
虽然会自动替你创建 model,但并不意味着你就万事大吉了,迁移数据模型的时候还是需要手动来管理
虽然编译能通过,但运行还是会崩溃,这是因为系统在尝试调用 Potato() 指定初始化时调用失败,managed objects 显然没有提供相关方法。
错误信息:Failed to call designated initializer on NSManagedObject class 'Potato'
用过 Core Data 的同学都知道,搭建 Core Data stack 是一件相当琐碎的事情,iOS 10 推出了新的 NSPersistentContainer 类封装好了那些单调而乏味的工作,并提供了一些便利方法和特性。
NSPersistentContainer 简化了创建和管理 Core Data stack 的过程,它会替我们创建 NSManagedObjectModel, NSPersistentStoreCoordinator, 以及 NSManagedObjectContext.
打开 AppDelegate.swift 导入 import CoreData,然后添加一个新属性:
var coreDataStack: NSPersistentContainer!
并且在 application(_:didFinishLaunchingWithOptions:)
的开始,添加
coreDataStack = NSPersistentContainer(name: "TaterRater")
这一步传入了之前我们创建的模型文件(TaterRater.xcdatamodeld)的名称,它会检索对应的模型,然后自动生成相应的 Core Data stack(这里是 NSPersistentContainer 类型),底层其实是创建了一个 persistent store coordinator。
其中一个有意思的新特性是:你可以让 persistent container 异步构建它的存储(store),如果有一个大数据或要做复杂迁移,使用异步不至于阻塞主线程。设置起来也非常简单:
coreDataStack.persistentStoreDescriptions.first? .shouldAddStoreAsynchronously = true
但在本工程中还是保持默认的同步设置(不做额外设置),接下来我们来指示 persistent container 加载持久存储区,完成 Core Data stack 的最终创建
coreDataStack.loadPersistentStores { description, error in if let error = error { print("Error creating persistent stores: / (error.localizedDescription)") fatalError() } }
上面的代码因为是同步加载,所以可能会需要时间,你需要在 UI 上做点等待动画。
最后将 let potato = Potato()
方法修改为使用 coreDataStack 来初始化并创建
let potato = Potato(context: coreDataStack.viewContext)
这行代码体现出 Core Data 的两个新特性:
再次运行,不会崩溃了。不过现在我们的数据源还是数组,接下来改造为使用 fetched results controller
打开 PotatoTableViewController.swift,除了导入 CoreData 之外,添加两个新属性来持有 fetched results controller 和 context
var resultsController: NSFetchedResultsController<Potato>! var context: NSManagedObjectContext!
我们发现 fetched results controllers 现在可以带类型了(泛型),继续添加以下代码到 viewDidLoad()
中:
// 1 这里的 fetchRequest() 实现系统已经替你实现好了: // NSFetchRequest<Potato>(entityName: "Potato"); let request: NSFetchRequest<Potato> = Potato.fetchRequest() // 2 这里使用了 `#keyPath` 语法糖,防止你输入错误 let descriptor = NSSortDescriptor(key: #keyPath(Potato.variety), ascending: true) // 3 将排序描述添加到 request 中 request.sortDescriptors = [descriptor] // 4 执行 resultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) do { try resultsController.performFetch() } catch { print("Error performing fetch /(error.localizedDescription)") }
最后将之前所有用数组做数据源的地方,替换为 resultsController 来实现
numberOfSections(in:) 下的
return resultsController.sections?.count ?? 0
tableView(_: numberOfRowsInSection:)
return resultsController.sections?[section].numberOfObjects ?? 0
configureCell(_: atIndexPath:)
let potato = resultsController.object(at: indexPath)
别忘了 prepare(for: sender:) 方法
detail.potato = resultsController.object(at: path)
最后你可以安全地删除 potatoes 属性数组了,此时还会报个错,切换到 AppDelegate.swift,替换报错行为如下代码:
potatoList.context = coreDataStack.viewContext
现在 table 在主线程上使用和导入对象相同的 managed object context 了。运行一下,确保现在是由 results-controller 驱动的数据源了,potatoes 常数因为不再使用,会报一个小警告。
results controller 更多功能还是要依赖它的代理才能实现,代理的 API 因为 Swift 3.0 语法有些调整外,和以前相比没太大变化
extension PotatoTableViewController: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.beginUpdates() } func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .delete: guard let indexPath = indexPath else { return } tableView.deleteRows(at: [indexPath], with: .automatic) case .insert: guard let newIndexPath = newIndexPath else { return } tableView.insertRows(at: [newIndexPath], with: .automatic) case .update: guard let indexPath = indexPath else { return } if let cell = tableView.cellForRow(at: indexPath) { configureCell(cell, atIndexPath: indexPath) } case .move: guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return } tableView.deleteRows(at: [indexPath], with: .automatic) tableView.insertRows(at: [newIndexPath], with: .automatic) } } func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.endUpdates() } }
返回 viewDidLoad()
,设置代理
resultsController.delegate = self
再次运行,调整评分星级也可以正常工作了
不过当前每次应用启动都会从头创建土豆列表,Core Data 一个特性就是可以持久化,所以我们要将设置好的结果保存起来。为了演示另一个新特性,我们要把创建工作挪到到后台执行。
在 Model 组下面添加一个新文件 PotatoTasks.swift,代码如下:
import CoreData extension NSPersistentContainer { func importPotatoes() { // 1 performBackgroundTask { context in // 2 let request: NSFetchRequest<Potato> = Potato.fetchRequest() do { // 3 if try context.count(for: request) == 0 { // TODO: Import some spuds } } catch { print("Error importing potatoes: /(error.localizedDescription)") } } } }
.count(for:)
是 countForFetchRequest(_: error:)
抛出异常的版本 替换 TODO 的注释部分,模拟一个从服务器上加载数据的过程:
sleep(3) guard let spudsURL = Bundle.main.url(forResource: "Potatoes", withExtension: "txt") else { return } let spuds = try String(contentsOf: spudsURL) let spudList = spuds.components(separatedBy: .newlines) for spud in spudList { let potato = Potato(context: context) potato.variety = spud potato.crowdRating = Float(arc4random_uniform(50)) / Float(10) } try context.save()
回到 AppDelegate.swift,在 application(_: didFinishLaunchingWithOptions)
中找到 loadPersistentStores(_:)
方法,在其后面修改为如下代码:
coreDataStack.importPotatoes() if let split = window?.rootViewController as? UISplitViewController { if let primaryNav = split.viewControllers.first as? UINavigationController, let potatoList = primaryNav.topViewController as? PotatoTableViewController { potatoList.context = coreDataStack.viewContext } split.delegate = self split.preferredDisplayMode = .allVisible
上面的代码把创建土豆的过程放到了后台执行,再次运行
咦?土豆在哪里?你可能会希望后台线程 context 保存后会过渡到主线程 context 来管理,通常我们会将后台 context 作为主线程 context 的孩子,但是 NSPersistentContainer 并没有替我们实现这一点。
如果你再次运行,会发现土豆又回来了,这会给我们一些启示。以前处理多个 managed object contexts 对象类似于下面这张图
这里只有唯一的 context 与 persistent store coordinator 对话,通常情况下它是一个后台 context,主要任务是负责执行保存。
而在主线程 context(View Context)之下的两个 Context: Editing Context 和 BG Context 都属于主线程 context 的孩子
这是非常必要的,因为 persistent store coordinator 和 SQL store 数据库在没有锁的情况下无法处理多个读写操作。
但在 iOS 10 中,SQL store 可以允许同时有多个读操作和单个写操作,persistent store coordinator 也不再需要锁了。这就意味着整个结构变成了下面这样:
persistent container 提供的后台 contexts 可以直接与 persistent store coordinator 进行对话,而不再需要作为主线程 context 的孩子,它可以直接通过 persistent store coordinator 写入 SQL 数据库,主线程 context 也无从知道保存究竟什么时候发生,除非重新运行 fetch requests 操作。
因此在旧版本的 iOS 上这会产生一个问题,我们可以通过监听的方式来告知主线程保存操作的发生。不过幸运的是,iOS 10 已经拿出了解决方案,在 AppDelegate.swift 中找到 importPotatoes()
,在该方法前添加:
coreDataStack.viewContext.automaticallyMergesChangesFromParent = true
这是 NSManagedObjectContext 的新属性,它替你做了所有的合并操作。
删除 App 后(清除数据)再次运行,这次会先看到一个空白的 tableview,但等一会后台 context 操作执行完成后,新的数据会自动在前台(主线程)显示。
iCloud Core Data 同步相关的方法都被移除掉了。因为 iCloud 和 Core Data 配合起来总有些问题,苹果最终决定放弃了。根据文档描述,现存的方法依然能工作,但用到 iCloud 的新项目还是不推荐 Core Data 了。
经过这些年的演变,苹果貌似打算将 Core Data 打造为一个更易使用的模型层,iCloud 的同步工作就交给 CloudKit 或其他的一些方法去处理吧。