原文:Multiple Managed Object Contexts with Core Data Tutoria
作者:Matthew Morey
译者:kmyhy

托管对象上下文是一个专门给托管对象使用的内存快照。

大多数 app 只需要一个托管对象上下文。大部分 Core Data 应用的默认配置都是附着在主线程上的单托管对象上下文。多托管对象上下文的 app 很难调试,它并不适用于所有 app 和所有场景。

也就是说,在特定情况下,才需要使用一个以上的托管对象上下文。例如,耗时任务,如数据的导出,如果在主线程中使用单托管对象上下文会阻塞 app 主线程,导致 UI 冻结。

另一方面,当编辑用户数据时,可能要将一部分改变放在一个托管对象上下文中,这样当不再需要它们时 app 可以放弃它们。这就会用到子上下文。

在本教程中,你将通过一个冲浪日记 app 学习多托管对象上下文,通过加入多个上下文,你将从几个方面改进这个 app。

注:这是一个进阶教程,本文假设你已经具备必要的 Swift、Core Data 和 iOS 开发技能。如果你不清楚基本 Core Data 概念比如托管对象子类、持久化存储协调器和 Corea Data stack,你可以读一读我们的其他 Core Data 教程。

开始

本教程从一个简单的冲浪日记 app 开始。在每次冲浪课结束后,选手可以利用这个 app 创建一条新的日志,用于保存海面参数,比如涌高和周期,以及对本次冲浪课按照从 1 到 5进行评价。如果你对冲浪运动毫无兴趣也没有关系。你可以将 app 中冲浪有关的内容修改成任何你喜欢的运动。

SurfJournal app 简介

从此处下载本教程的开始项目 SurfJournal。打开项目,编译运行。

一开始,app 会列出之前记录的所有冲浪课记录。点击 cell,将跳到查看和编辑冲浪课的详细视图。

如你所见,这个 app 能够运行,而且还能显示出数据。点击左上角的 Export 按钮可以将数据导出为 CSV 文件。点击右上角的 + 按钮可以新建一条日志。点击列表中的一行,会进入编辑模式,你可以修改或查看冲浪课详情。

虽然示例项目很简单,但它已经够用了,我们将以此为基础,在它原有的基础上添加对多上下文的支持。首先,让我们好好对项目中的各个类进行一个了解。

打开项目导航窗口,看一下开始项目中包含的文件:

在查看代码之前,先简单介绍一些各个类的大致功能:

  • AppDelegate: 在第一次启动时,app delegate 会创建一个 Core Data Stack,并赋给主视图控制器 JournalListViewController 的 coreDataStack 属性。
  • CoreDataStack: 这个对象是 Core Data 对象容器,类似于堆栈。这个堆栈会在第一次运行时安装一个包含了数据的数据库。先不用管它,随后你会看到它是如何工作的。
  • JournalListViewController: 示例程序是一个基于表格的单页应用。这个文件负责呈现表格。如果想了解它的 UI 组件,你可以看一下 Main.storyboard。有一个嵌在导航控制器中的表视图控制器,上面会有一个模板单元格,是 SurfEntryTableViewCell 类型。
  • JournalEntryViewController: 这个类负责创建和编辑冲浪日记。你可以在 Main.storyboard 中查看它的 UI 。
  • JournalEntry: 这个类代表一条冲浪日记。它是 NSManagedObject 子类,拥有 6 个属性:日期、高度、地点、周期、速度和风力。如果你想了解这个类的定义,请打开 SurfJournalModel.xcdatamodel。
  • JournalEntry+Helper: JournalEntry 类的扩展。包含了 csv() 方法和 stringForDate() 方法,前者用于导出 cvs 文件。这些方法放到扩展中,目的是避免修改 Core Data 模型后这些方法被覆盖。

当你第一次启动 app 时,app 中就已经有大量数据了,这是因为这个示例项目有一个 Core Data 种子数据库。

Core Data Stack

打开 CoreDataStack.swift 找到 seedCoreDataContainerIfFirstLaunch() 方法中的下列代码:

“`swift
// 1
let previouslyLaunched =
UserDefaults.standard.bool(forKey: “previouslyLaunched”)
if !previouslyLaunched {
UserDefaults.standard.set(true, forKey: “previouslyLaunched”)

// Default directory where the CoreDataStack will store its files
let directory = NSPersistentContainer.defaultDirectoryURL()
let url =
directory.appendingPathComponent(modelName + “.sqlite”)

// 2: 拷贝 SQLite file 文件
let seededDatabaseURL =
Bundle.main.urlForResource(modelName,
withExtension: “sqlite”)!
_ = try? FileManager.default.removeItem(at: url)
do {
try FileManager.default.copyItem(at: seededDatabaseURL,
to: url)
} catch let nserror as NSError {
fatalError(“Error: (nserror.localizedDescription)”)
}


看到了吧,本教程的 CoreDataStack.swift 有一点与众不同:1. 首先判断 User Defaults 中的 previouslyLaunched 布尔变量。如果 app 是第一次运行,这个 Bool 值应该是 false,if 语句的检查结果为 true。当第一次运行,我们首先将 previouslyLanuched 设置为 true, 目的是为了让后面初始化动作不会再次执行。
2. 复制初始的 SQLite 数据库文件 SurfJournalModel.sqlite。这个文件在 app 的 bundle 中,我们将它拷贝到 NSPersistentContainer.defaultDirectoryURL() 指定的目录。继续看 seedCoreDataContainerIfFirstLaunch() 方法中后面的代码:```swift// 3: 拷贝 SHM 文件let seededSHMURL = Bundle.main.urlForResource(modelName,withExtension: "sqlite-shm")!let shmURL = directory.appendingPathComponent(modelName + ".sqlite-shm")_ = try? FileManager.default.removeItem(at: shmURL)do {try FileManager.default.copyItem(at: seededSHMURL,to: shmURL)} catch let nserror as NSError {fatalError("Error: \(nserror.localizedDescription)")}// 4: 拷贝 WAL 文件let seededWALURL = Bundle.main.urlForResource(modelName,withExtension: "sqlite-wal")!let walURL = directory.appendingPathComponent(modelName + ".sqlite-wal")_ = try? FileManager.default.removeItem(at: walURL)do {try FileManager.default.copyItem(at: seededWALURL,to: walURL)} catch let nserror as NSError {fatalError("Error: \(nserror.localizedDescription)")}print("Seeded Core Data")
}<div class="se-preview-section-delimiter"></div>

一旦拷贝 sqlite 文件成功,我们接着拷贝支持文件 SurfJournalModel.sqlite-shm。

最后,还要拷贝另一个支持文件 SurfJournalModel.sqlite-wal。

如果这 3 个文件拷贝失败,说明某些极端的情况发生了,比如磁盘被宇宙辐射毁坏。在这种情况下,设备以及所有的 app 都没法用了。如果文件拷贝不成功,就不可能继续运行了,因此在 catch 块中调用了 fatalError。

注:开发者通常不喜欢 abort 和 fatalError,因为它们都会导致 app 突然退出,没有任何提示。在这里使用 fatalError 是合理的,因为 app 需要 Core Data 才能工作。如果 app 需要使用 Core Data 而 Core Data 根本无法工作,则不要让 app 继续运行,否则说不清什么时候 app 会以一种不确定的方式出错。

调用 fatalError,至少会产生一个调用堆栈,这对于解决问题多少有点用处。如果 app 支持远程的日志存储或者崩溃报告,你可以将有关信息记录下来,找出调用 fatalError 之前发生了什么。

为了支持同步读和写,示例中的持久化 SQLite 数据库使用了 SHM(共享内存文件)和 WAL(预写日志) 文件。你无需关心这两个文件的作用,你只需要知道有这么两个文件存在,当你还原种子数据库时你必须复制它们就可以了。如果你拷贝这两个文件失败,app 仍然能够工作,但数据可能会丢失。

现在你准备好使用种子数据库了,接下来用一个临时的私有上下文来学习多托管对象上下文。

使用后台线程

如果你还没有这么做的话,那么请点击左上角的 Export 按钮,并立即滚动表格。注意到了吗?导出操作需要几秒钟的时间去完成,它冻住了 UI 对滚动等触摸动作的响应。

在导出操作时,UI 被阻塞,因为导出操作和 UI 都使用主线程执行。这是默认的行为。

传统的方法是用 GCD 将导出操作放到后台线程中进行。但是,Core Data 托管对象上下文不是线程安全的。意思是,你无法在后台线程中执行操作同时还要使用同一个 Core Data stack。

解决方法很简单:用一个私有的后台线程,而不是主线程去进行导出。这会让主线程不被阻塞。

但在解决这个问题之前,你需要理解导出操作是怎样执行的。

导出数据

看一下 app 是如何生成 JournalEntry 对象的 csv 字符串的。打 JournalEntry+Helper.swift 找到 csv() 方法:

func csv() -> String {let coalescedHeight = height ?? ""let coalescedPeriod = period ?? ""let coalescedWind = wind ?? ""let coalescedLocation = location ?? ""let coalescedRating: Stringif let rating = rating?.int32Value {coalescedRating = String(rating)} else {coalescedRating = ""}return "\(stringForDate()),\(coalescedHeight),\(coalescedPeriod), \(coalescedWind),\(coalescedLocation),\(coalescedRating)\n"
}<div class="se-preview-section-delimiter"></div>

如你所见,JournalEntry 返回一个逗号分隔的属性组成的字符串。因为 JournalEntry 的属性都是可空的,在函数中我们使用“空合并”(??) 运算符将 nil 替换为空字符串,从而避免调试信息中出现无意义的 attribute is nil 消息。

注:空合并(??)操作在一个可空类型有值时进行解包,无值时返回默认值。例如: let coalescedHeight = height != nil ? height! : “” 用空合并操作可以精简为: let coalescedHeight = height ?? “” 。

这是 app 将一个单个的 JournalEntry 对象转换成 CVS 字符串,那么 app 又是如何将 CSV 文件保存到磁盘的呢?打开 JournalListViewController.swift 找到 exportCSVFile() 方法中的如下代码:

// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {print("ERROR: \(error.localizedDescription)")
}// 2
let exportFilePath = NSTemporaryDirectory() + "export.csv"
let exportFileURL = URL(fileURLWithPath: exportFilePath)
FileManager.default.createFile(atPath: exportFilePath,contents: Data(), attributes: nil)<div class="se-preview-section-delimiter"></div>

CSV 的导出代码分别解释如下:

  1. 首先,通过一个 fetch 抓取全部的 JournalEntry 对象。这个 fetch 请求和负责处理抓取结果的控制器的 fetch 请求是一样的,因此你可以重用 surfJournalFetchRequest 方法来创建请求,避免写同样的代码。

  2. 接着,创建一个要导出的 CSV 文件路径 URL,即在 NSTemporaryDirectory 方法后面添加文件名 export.csv。

    NSTemporaryDirectory 方法返回一个唯一的临时文件目录。这个目录中的文件能够轻易重建,而且不会被 iTunes 或者 iCloud 文件同步。

    创建好导出文件的 URL 后,调用 createFileAtPath(_:contents:attributes:) 创建一个空文件,用于保存所导出的数据。如果文件已经存在,这个方法会先删除文件。

    当 app 创建好空文件后,就可以将 CSV 数据保存到磁盘上了:

    // 3
    let fileHandle: FileHandle?
    do {
    fileHandle = try FileHandle(forWritingTo: exportFileURL)
    } catch let error as NSError {
    print("ERROR: \(error.localizedDescription)")
    fileHandle = nil
    }
    if let fileHandle = fileHandle {
    // 4
    for journalEntry in results {
    fileHandle.seekToEndOfFile()
    guard let csvData = journalEntry.csv().data(using: .utf8, allowLossyConversion: false) else {continue
    }fileHandle.write(csvData)
    }// 5
    fileHandle.closeFile()print("Export Path: \(exportFilePath)")
    self.navigationItem.leftBarButtonItem =
    self.exportBarButtonItem()
    self.showExportFinishedAlertView(exportFilePath)
    } else {
    self.navigationItem.leftBarButtonItem =
    self.exportBarButtonItem()
    }

    解释一下文件处理步骤:

  3. 首先,app 需要创建一个 file handler 用于写入操作,这是一个用于处理低级磁盘写入操作的简单对象。要创建一个 file handler,请使用 FileHandle(forWritingTo:) 初始化方法。

  4. 然后,遍历所有的 JournalEntry 对象。在每次遍历的时候,需要调用 JournalEntry 的 cvs() 方法获得一个 UTF8 编码的 String,然后在这个 String 上调用 data(using:allowLossyConversion:) 方法。如果成功,将 UTF8 字符串用 file handler 的 write 方法写入磁盘。
  5. 最后,关闭 file handler,因为它的使命结束了。

如果 app 将所有数据写入磁盘,它会显示一个消息框,提示文件导出路径:

注:用消息框弹出导出路径只是用于练习目的,在真正的 app,你需要提供一个能够打开导出的 CSV 文件的方式给用户,比如使用 UIActivityViewController。

要打开 CSV 文件,可以用 Excel、Numbers 或者任意文本编辑器。在 Numbers 中打开 CSV 文件的效果如下:

现在,你已经了解 app 是如何导出数据的了,接下来就应该进行某些改进了。

在后台进行导出

当导出进行时,你希望 UI 能够继续响应用户动作。要解决这个问题,你可以将导出动作放到一个私有的后台上下文中,而不要在主上下文中。

打开 JournalListViewController.swift 找到 exportCSVFile() 方法中的下列代码:

// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {print("ERROR: \(error.localizedDescription)")
}<div class="se-preview-section-delimiter"></div>

就像你之前看到的,这段代码调用 fetch 方法在托管对象上下文中检索每个 JournalEntry 对象。

将这段代码修改为:

// 1
coreDataStack.storeContainer.performBackgroundTask {(context) invar results: [JournalEntry] = []do {results = try context.fetch(self.surfJournalFetchRequest())} catch let error as NSError {print("ERROR: \(error.localizedDescription)")}<div class="se-preview-section-delimiter"></div>

替代原来的同样被 UI 所使用的主托管对象上下文,我们改用 performBackgroundTask(_:) 方法。这会创建一个私有的上下文,代码块将放到这个上下文中执行。

用 performBackgroundTask(_:) 所创建的私有上下文是放在私有线程中的,它不会阻塞 UI 主线程。这个方法的替代品是通过并行类型 PrivateQueueConcurrencyType 手动新建一个临时的私有上下文。

注:一个托管对象上下文可以使用 3 种并行类型:

  • ConfinementConcurrencyType : 表明上下文将开启受限的线程模式,开发者负责管理所有的线程访问。你应当慎用或不用这种类型,只使用后面两种类型就够了。
  • PrivateQueueConcurrencyType : 表明上下文将使用私有的 dispatch queue 而不是 main queue。这种类型的 queue 就是我们在导出数据时从主线程中移入到的 queue,因此它不会阻塞 UI。
  • MainQueueConcurrencyType:默认的类型,表明上下文会使用主线程。这种类型就是主上下文(coreDataStack.mainContext) 所采用的类型。任何 UI 操作,比如为了展现表格而负责接收抓取结果的控制器,必须使用这种类型。

然后,在同一方法中找到这段代码:

  print("Export Path: \(exportFilePath)")self.navigationItem.leftBarButtonItem =self.exportBarButtonItem()self.showExportFinishedAlertView(exportFilePath)
} else {self.navigationItem.leftBarButtonItem =self.exportBarButtonItem()
}<div class="se-preview-section-delimiter"></div>

替换为:

    print("Export Path: \(exportFilePath)")// 6DispatchQueue.main.async {self.navigationItem.leftBarButtonItem =self.exportBarButtonItem()self.showExportFinishedAlertView(exportFilePath)}} else {DispatchQueue.main.async {self.navigationItem.leftBarButtonItem =self.exportBarButtonItem()}}
} // 7 关闭 performBackgroundTask<div class="se-preview-section-delimiter"></div>

完成最后的工作:

6. 你应当将所有的 UI 相关操作放到主线程中,比如导出结束时显示一个消息框。否则会发生一些不好的事情。使用 DispatchQueue.main.async 方法在主线程中显示最后的消息框。

7. 最后,添加一个右花括号以和你在第一步中调用 performBackgroundTask(_:) 的左花括号配对。

将导出操作放到使用了私有线程队列的新的上下文中。编译运行,看看是否 OK ?

这个界面和前面一样,没有变化:

点击 Export 按钮,立即滚动表格。注意这次有什么不同了吗?导出操作仍然要花那么多时间才能完成,但表格这次滚动不受影响了。导出操作不再阻塞 UI。

伙计,干得漂亮!现在 UI 变得更加响应式了。

刚才你已经看到了通过私有后台线程队列能够改善 app 的用户体验。接下来,你将看到另一个多上下文的例子,即子上下文的使用。

在内存快照中进行编辑

当前,SurfJournal 在新建和查看详情时使用主上下文 (coreDataStack.mainContext)。这种方法本身没有什么错,开始项目原模原样地使用了这种方法。

对于这种类型的日志类 app,你可以简单地将新建和编辑都视作某种修改,就像便签本一样。当用户编辑日记内容时,你修改了托管对象的属性,一旦修改完成,根据用户的选择,你要么保存修改,要么舍弃修改。

你可以将子托管上下文看成是一种临时的便签本,你可以完全扔掉它,或者保存它,将改变提交给父上下文。

从技术上而言,什么是子上下文?

所有的托管对象上下文都有一个父容器,你可以通过它来查看和修改父容器中的数据,这些数据是一些托管对象,比如在本例中,就是 JournalEntyry 对象。一般,这个父容器是一个持久化存储协调器,也就是 CoreDataStack 类提供给我们的主上下文。某些时候,你也可以将某个上下文的父容器设置为另一个托管对象上下文,也就是将它作为另一个上下文的子上下文。

当你保存子上下文时,仅仅是将修改传递到父上下文。这些改变到了父上下文后,并不会提交给持久化存储协调器,一直到父上下文保存时。

在你准备添加一个子上下文之前,你需要知道当前的浏览和编辑操作是如何工作的。

查看和编辑

这个操作的第一个导航动作是从主列表跳转到详情页面。打开 JournalListViewController.swift 找到 prepareForSegue(for:sender:)方法:

// 1
if segue.identifier == "SegueListToDetail" {// 2guard let navigationController =segue.destination as? UINavigationController,let detailViewController =navigationController.topViewControlleras? JournalEntryViewController,let indexPath = tableView.indexPathForSelectedRow else {fatalError("Application storyboard mis-configuration")}// 3let surfJournalEntry =fetchedResultsController.object(at: indexPath)//4detailViewController.journalEntry = surfJournalEntrydetailViewController.context =surfJournalEntry.managedObjectContextdetailViewController.delegate = self<div class="se-preview-section-delimiter"></div>

这个跳转动作分成几个步骤:

  1. 有两个 segue:SegueListToDetalil 和 SegueListToDetailAdd。首先,看一下前者的代码块,当用户点击表格中的某一行或者编辑已有的日志记录时执行这个 segue。
  2. 然后,获得一个 JournalEntryViewController 对象,也就是用户即将看到的页面。这个页面是放在一个导航控制器中的,因此要从导航控制器中取出来。这段代码会对表格是否存在已选的 IndexPath 进行检查。
  3. 获取用户选中的 JournalEntry 对象,使用的是 fetchedResultsController 的 object(at:) 方法。
  4. 最后,检索 JournalEntry 对象属性并赋给 JournalEntryViewController。surfJournalEntry 变量就是第 3 步中获得 JournalEntry 实体对象。context 属性是在所有操作中都要用到的托管对象上下文;当前,它使用的就是主上下文。JournalListViewController 将自己作为 JournalEntryViewController 的 delegate,这样当用户完成编辑后它能够知道。

SegueListToDetailAdd 和 SegueListToDetail 类似,但它是用于新建日记而不是查看已有的日记。当用户点击右上角的 + 号按钮去新建一个日记时,这个 segue 会执行。

现在两个 segue 都已经搞清楚了,回到 JournalEntryViewController.swift,在文件头部是 JournalEntryDelegate 委托协议:

protocol JournalEntryDelegate {func didFinish(viewController: JournalEntryViewController,didSave: Bool)
}<div class="se-preview-section-delimiter"></div>

如你所见,JournalEntryDelegate 协议只包含了一个方法:didFinish(viewController:didSave:)。委托对象必须实现这个方法,通过这个方法告诉 app 用户已经编辑或查看完某个日记了,以及是否有需要保存的修改。

看一下 didFinish(viewController:didSave:) 怎么用吧。回到 JournalListViewController.swift 并找到这个方法:

func didFinish(viewController: JournalEntryViewController,didSave: Bool) {// 1guard didSave,let context = viewController.context,context.hasChanges else {dismiss(animated: true)return}// 2context.perform {do {try context.save()} catch let error as NSError {fatalError("Error: \(error.localizedDescription)")}// 3self.coreDataStack.saveContext()}// 4dismiss(animated: true)
}<div class="se-preview-section-delimiter"></div>

这个方法可以分成了几个步骤:

  1. 首先,用一个 guard 语句对 didSave 进行判断。如果用户点击了 Save 按钮而不是 Cancel 按钮,那么这个参数应该是 true。这样 app 知道用户做了修改,需要保存。guard 语句还通过 hasChange 属性检查是否有东西被修改过,如果没有任何东西被修改过,就不需要浪费时间了。

  2. 然后,在 perform 方法的闭包中保存 JournalEntryViewController 上下文。代码中将 context 设为主上下文;其实这是不必要的,因为只有一个上下文,不过这也无所谓。当后面我们添加子上下文时,JournalEntryViewController 的 context 不再是主上下文,那么这句代码就要用上了。如果保存失败,调用 fatalError 去退出 app,并打印相应的错误信息。

  3. 接着,调用 saveContext 保存主上下文,这个方法在 CoreDataStack.swift 中,将修改持久化到磁盘。

  4. 追后,解散 JournalEntryViewController。

注:如果托管对象上下文的类型是 MainQueueConcurrencyType,你就不需要将代码放在 perform(_:) 中, 当然你这样做也没关系。
如果你不知道上下文的类型,则在 didFinish(viewController:didSave:) 调用 perform(:) 方法是最安全的做法,因此无论是在父上下文还是子上下文都会执行。

上面的实现有一个问题—— 你发现了吗?

当 app 新建一个日记时,它创建一个新对象,将它加到托管对象上下文中。如果用户点击了 Cancel 按钮,app 不会保存上下文,但新对象仍然还会显示。如果用户这时再新建一个日记并保存,这个被 Cancel 掉的对象仍然还会被显示出来。你可能无法在 UI 中看见它,除非你有耐心将表格拉到最下面,如果查看导出的 CSV 文件它也会显示在最后面。

你可以在用户点击 Cancel 时删除这个对象,以解决这个问题。但如果修改比较复杂的时候,比如涉及到多个对象,或者在一系列操作中,一次只修改一个对象的属性时怎么办?使用子上下文是一种简单的办法。

在修改中使用子上下文

新建和编辑 JournalEntry 的方式搞清楚了,接下来是在修改中实现,将子托管对象上下文作为临时的便签。

其实很简单——你只需要修改 segue。打开 JournalListViewController.swift 在 prepareForSegue(for:sender:) 方法中找到跳转到 SegueListToDetail 的 代码:


detailViewController.journalEntry = surfJournalEntry
detailViewController.context =surfJournalEntry.managedObjectContext
detailViewController.delegate = self<div class="se-preview-section-delimiter"></div>

将这段代码替换为:

Next, replace that code with the following:
// 1
let childContext =NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = coreDataStack.mainContext// 2
let childEntry =childContext.object(with: surfJournalEntry.objectID)as? JournalEntry// 3
detailViewController.journalEntry = childEntry
detailViewController.context = childContext
detailViewController.delegate = self

分步骤解说如下:

  • 首先,你创建了一个新的托管对象上下文,名叫 childContext,类型为 MainQueueConcurrencyType。然后设置它的父上下文,这和以前新建上下文时不同,那种情况下通常会使用一个持久化存储协调器。
  • 接着,用子上下文的 object(with:) 方法检索出对应的 JournalEntry 对象。你必须用这个方法去取出 JournalEntry 对象,因为我们想使用这个托管上下文来创建 JournalEntry 对象。但是,objectID 值并不会和单个上下文相关联,因此你可以在多上下文环境中使用 objectID 来访问对象。
  • 最后,设置 JournalEntryViewController 的属性。这次,你使用了 childEntry 和 childContext 来替代原来的 surfJournalEntry 和 surfJournalEntry.managedObjectContext。

注:你可能不明白为什么既要传递托管对象,又要传递托管对象上下文给 detailViewController,在托管对象中不是已经有一个 context 属性了吗?这是因为托管对象只持有一个 context 的弱引用。如果你不传递 context,ARC 会从内存中删除 context(因为没有任何对象 retain 它),导致 App 的行为可能出乎你的预料。

运行 App,它仍然同以前一样工作。这次,有一个好消息,那就是 App 没有任何明显的改变,用户仍然可以通过点击单元格去查看和编辑冲浪课的 JournalEntry 数据。

通过用子上下文作为编辑日记的容器,你可以降低 App 的复杂度。通过在一个单独的 context 上进行编辑,取消保存托管对象变得很容易。

伙计,干得不错!当面对多托管对象上下文时,你不再是一只菜鸟了!放心大胆去用吧!

结束

你可以下载最终完成的项目。

如果你顺利学完这一章,你已经能够将一个使用单上下文的 App 转换成多上下文 App 了。你可以在这里找到本章的最终项目。

你通过在一个私有后台托管上下文中执行导出操作来改善 UI 是响应,通过使用子上下文来改进 App 的结构,同时还可以将子上下文当做一个临时的便签本。

但最妙的一件事情是,你学会了如何像一个冲浪者一样说话。那是你忙了整整一天的收获 :]

如果你喜欢这篇教程,你可能也会喜欢我们的这本《Core Data by Tutorials》。

这本书深入介绍了 Core Data 的技术细节,它是专门为已经了解基本的 iOS 和 Swift 开发但想学习如何使用 Core Data 存储 App 中的数据的中级 iOS 开发者准备的。

它已经完全更新为 Swift 3,iOS 10 和 Xcode 8 ——立即去 raywenderlich.com 商店中看看吧!

Core Data 教程:多托管对象上下文相关推荐

  1. Core Data 教程入门

    原文:Getting Started with Core Data Tutorial 作者:Pietro Rea 译者:kmyhy 这是<Core Data by Turoials>一书的 ...

  2. Core Data 教程(2): 如何预载/导入已有的数据

    这是系列教程的第二部分,有助于你加快掌握基本的Core Data内容. 在系列教程一中,我们为对象建立了可视化数据模型,运行了快速肮脏测试并勾在一个表视图(table view)中来显示.而在这个教程 ...

  3. iOS数据持久化 -- Core Data

    Core Data是一个功能强大的层,位于SQLite数据库之上,它避免了SQL的复杂性,能让我们以更自然的方式与数据库进行交互.Core Data将数据库行转换为OC对象(托管对象)来实现,这样无需 ...

  4. iOS教程:Core Data数据持久性存储基础教程

    目录[-] 创建Core Data工程 创建数据模型 测试我们的数据模型 来看看SQL语句的真面目 自动生成的模型文件 创建一个表视图 之后看些什么? 就像我一直说的,Core Data是iOS编程, ...

  5. 2. 托管对象数据模型的基本知识(Core Data 应用程序实践指南)

    第一章的例子配置好了持久化存储区.持久化存储协调器.托管对象上下文.但是还没有对象图,本章要介绍托管对象模型的基础知识,并配置范例程序的对象图. 2.1. 托管对象模型是什么 托管对象模型是一种数据结 ...

  6. core data使用教程

    core data使用教程 从印象中记得还是在学校的时候老师讲过的时候用过,那时觉得好难,以至于工作2年多了一直没敢去看core data,前几天想了下,不去看不行,得都会用才行,于是那天6点下班后就 ...

  7. ASP.NET CORE 入门教程(附源码)

    ASP.NET CORE 入门教程 第一课 基本概念 基本概念 Asp.Net Core Mvc是.NET Core平台下的一种Web应用开发框架 符合Web应用特点 .NET Core跨平台解决方案 ...

  8. Core Data 学习笔记(二)被管理对象模型

    为什么80%的码农都做不了架构师?>>>    目录 Core Data 学习笔记(一)框架简介 Core Data 学习笔记(二)被管理对象模型 Core Data 学习笔记(三) ...

  9. Core Data的使用

    初步看了一下Core Data这个东西,本想早一点写这篇东西的,不过各种俗事缠身,又觉得自己对于Core Data机制了解的还不够深,动笔就慢了几天.不过今天盘点一下,觉得可以说一点东西出来就先说一点 ...

最新文章

  1. 生猛!PDF 版本 6000 页 Java 手册开放下载!
  2. 基于postfix一步一步构建Mailserver,支持虚拟用户,支持WebMail
  3. 【数据结构作业心得】纸面6 - Matlab LU分解
  4. Service中的绑定服务总结
  5. Linux进程状态解析 之 R、S、D、T、Z、X (主要有三个状态)
  6. 教你如何在Android 6.0上创建系统悬浮窗
  7. Spring-beans-BeanFactoryPostProcessor
  8. .Net 中的反射(序章) - Part.1
  9. 【优化算法】改进型的LMS算法-SVSLMS算法【含Matlab源码 632期】
  10. 桂林理工大学计算机院导师信息,2018年新增硕士研究生指导教师名单公示
  11. 数据分析——人口变化matplotilb绘图
  12. DQ77KB刷bios工程小记-old文章备份
  13. 400GE燎原前夜,智能IP网络的核心路由器巅峰际会
  14. USB CCID理解
  15. 如何做好一个产品经理
  16. python股票接口_在Python中使用股票接口
  17. java8 .stream().anyMatch / allMatch / noneMatch用法
  18. 数据库 关系模式和关系的区别
  19. HX711压力传感器学习(STM32)
  20. AI数学基础(2)--- 霍夫丁不等式

热门文章

  1. 离散对数和椭圆曲线加密原理
  2. 我国数据安全法详细解读
  3. Python读取PSV
  4. Unity中实现声音的近大远小
  5. 在64位Ubuntu 16.04系统里安装Qt 5.9.1
  6. 服务器u用固态硬盘,Bluehost SSD固态硬盘服务器性能评测
  7. 源码到底应该怎么读?
  8. Office 彻底卸载
  9. 7、中置、一元、赋值、结合、apply和update、unapply提取器
  10. 《Semi-Supervised Semantic Segmentation with Cross-Consistency Training》 2020CVPR 论文阅读