9.多个托管对象上下文¶
托管对象上下文是用于处理托管对象的内存中暂存器。 在第3章“核心数据栈”中,您学习了托管对象上下文如何与核心数据栈中的其他类相适应。
大多数应用程序只需要一个托管对象上下文。 大多数Core Data
应用程序中的默认配置是与主队列关联的单个托管对象上下文。 多个托管对象上下文使您的应用程序更难调试;这不是你在每一个应用程序中,在每一种情况下都会用到的东西。
话虽如此,在某些情况下确实需要使用多个托管对象上下文。 例如,长时间运行的任务(如导出数据)将阻塞仅使用单个主队列托管对象上下文的应用程序的主线程,并导致UI
卡顿。
在其他情况下,例如对用户数据进行编辑时,将托管对象上下文视为一组更改会很有帮助,应用可以在不再需要这些更改时丢弃这些更改。 使用子上下文使这成为可能。
在本章中,您将通过为冲浪者提供一个日志应用程序并通过添加多个上下文以多种方式对其进行改进来了解多个托管对象上下文。
Note
如果常见的核心数据短语,如托管对象子类和持久化存储协调器没有任何铃声,或者如果你不确定核心数据堆栈应该做什么,你可能想在继续之前阅读或重读本书的前三章。 本章介绍高级主题,并假定您已经了解基本知识。
入门¶
这一章的入门项目是一个简单的日志应用程序的冲浪。 在每次冲浪后,冲浪者可以使用该应用程序创建一个新的日志条目,记录海洋参数,如涌浪高度或周期,并从1到5对会话进行评级。 伙计,如果你不喜欢吊十个和获得桶,不用担心,兄弟。 只要用你最喜欢的爱好来代替冲浪术语!
SurfJournal
简介¶
转到本章的文件并找到 SurfJournal的·项目。 打开项目,然后生成并运行应用程序。
启动时,应用程序会列出所有以前的冲浪会话日志条目。 点击行将显示具有编辑功能的冲浪会话的详细视图。
正如您所看到的,示例应用程序可以正常工作并具有数据。 点击左上角的导出按钮,将数据导出到逗号分隔值(·)文件。 点击右上角的加号(+)按钮可添加新的日记条目。 点击列表中的一行可在编辑模式下打开条目,您可以在其中更改或查看冲浪会话的详细信息。
尽管示例项目看起来很简单,但它实际上做了很多事情,可以作为添加多上下文支持的良好基础。 首先,让我们确保您对项目中的各个类有一个很好的理解。
打开项目导航器,查看启动项目中的完整文件列表:
在开始编写代码之前,先花一点时间回顾一下每个类的开箱即用功能。 如果您已经完成了前面的章节,您应该会发现这些类中的大多数都很熟悉:
- AppDelegate:在第一次启动时,应用委托创建
Core Data
堆栈并设置主视图控制器JournalListViewController
上的coreDataStack
属性。 - CoreDataStack:和前面章节一样,这个对象包含核心数据对象,称为 stack 。 与前几章不同的是,这一次堆栈安装的数据库在第一次启动时就已经有了数据。 现在还不用担心这个;你很快就会看到它是如何工作的。
- JournalListViewController:示例项目是一个单页的基于表的应用程序。 此文件代表该表。 如果你对它的UI元素感兴趣,请前往 Main.storyboard 。 有一个嵌入在导航控制器中的表视图控制器和一个类型为 SurfEntryTableViewCell 的单个原型单元。
- JournalEntryViewController:此类处理创建和编辑冲浪日志条目。 您可以在 Main.storyboard 中看到它的界面。
- JournalEntry:此类表示冲浪日志条目。 它是一个
NSManagedObject
子类,有六个属性:date
、height
、location
、period
、rating
和wind
。 如果你对这个类的实体定义感兴趣,请查看 SurfJournalModel。xcdatamodel。
- JournalEntry+Helper:这是
JournalEntry
对象的扩展。 它包括CSV
导出方法csv()
和stringForDate()
的helper
方法。 这些方法在扩展中实现,以避免在更改Core Data
模型时被破坏。
当你第一次启动应用程序时,已经有了大量的数据。
虽然前面一些章节中的项目从JSON
文件导入种子数据,但此示例项目附带了种子Core Data
数据库。
核心数据栈¶
打开 CoreDataStack。swift,在seedCoreDataContainerIfFirstLaunch()
中找到以下代码:
// 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: Copying the SQLite file
let seededDatabaseURL = Bundle.main.url(
forResource: 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 版本有点不同:
- 首先检查
UserDefaults
以获取previouslyLaunched
布尔值。 如果当前执行确实是应用程序的第一次启动,则Bool
将为false
,使if
语句为true
。 在第一次启动时,您要做的第一件事是将previouslyLaunched
设置为true
,这样播种操作就不会再次发生。 - 然后复制
SQLite
种子文件 SurfJournalModel.sqlite,包含在应用包中,复制到Core Data
提供的方法NSPersistentContainer返回的目录中。defaultDirectoryURL()
。
现在查看seedCoreDataContainerIfFirstLaunch()
的其余部分:
// 3: Copying the SHM file
let seededSHMURL = Bundle.main.url(forResource: 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: Copying the WAL file
let seededWALURL = Bundle.main.url(forResource: 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")
}
- 一次 SurfJournalModel的副本。sqlite 已成功,然后复制支持文件 SurfJournalModel。sqlite-shm.
- 最后,复制剩余的支持文件 SurfJournalModel。sqlite-wal.
唯一的原因是 SurfJournalModel.sqlite,SurfJournalModel.sqlite-shm 或 SurfJournalModel.sqlite-wal 在第一次启动时复制失败是因为发生了一些非常糟糕的事情,例如宇宙辐射导致的磁盘损坏。 在这种情况下,设备,包括任何应用程序,也可能会失败。 如果文件复制失败,继续复制就没有意义了,所以catch
块调用fatalError
。
Note
开发人员通常不喜欢使用abort
和fatalError
,因为它会导致应用程序突然退出而没有解释,从而使用户感到困惑。 这是一个fatalError
是可以接受的情况,因为应用程序需要Core Data
才能工作。 如果一个应用程序需要核心数据,而核心数据不起作用,那么让应用程序继续运行是没有意义的,只会在某个时候以不确定的方式失败。
调用fatalError
至少会生成一个堆栈跟踪,这在尝试修复问题时可能很有帮助。 如果您的应用支持远程日志记录或崩溃报告,则应在调用fatalError
之前记录可能有助于调试的任何相关信息。
为了支持并发读取和写入,此示例应用程序中的持久性SQLite
存储使用SHM
(共享内存文件)和WAL
(预写日志记录)文件。 您不需要知道这些额外的文件是如何工作的,但是您需要知道它们的存在,并且在播种数据库时需要复制它们。 如果您无法复制这些文件,应用程序将工作,但它可能会丢失数据。
现在您已经了解了从种子数据库开始的一些情况,接下来将通过处理临时私有上下文来了解多个托管对象上下文。
后台工作¶
如果您还没有这样做,点击左上角的 Export 按钮,然后立即尝试滚动冲浪会话日志条目列表。 注意到什么了吗 导出操作需要几秒钟的时间,并且会阻止UI
响应触摸事件(如滚动)。
UI
在导出操作期间被阻止,因为导出操作和UI
都在使用主队列执行其工作。 这是默认行为。
解决此问题的传统方法是使用Grand Central Dispatch
在后台队列上运行导出操作。 但是,Core Data
托管对象上下文不是线程安全的。 这意味着你不能只是分派到后台队列,并使用相同的核心数据堆栈。
解决办法很简单:使用专用后台队列而不是主队列进行导出操作。 这将使主队列保持空闲,以供UI使用。 但是在开始修复问题之前,您需要了解导出操作是如何工作的。
导出数据¶
首先查看应用程序如何为JournalEntry
实体创建CSV
字符串。 打开 JournalEntry+Helper.swift 并查找csv()
:
func csv() -> String {
let coalescedHeight = height ?? ""
let coalescedPeriod = period ?? ""
let coalescedWind = wind ?? ""
let coalescedLocation = location ?? ""
let coalescedRating: String
if let rating = rating?.int16Value {
coalescedRating = String(rating)
} else {
coalescedRating = ""
}
return [
stringForDate(),
coalescedHeight,
coalescedPeriod,
coalescedWind,
coalescedLocation,
coalescedRating,
"\n"
].joined(separator: ",")
}
如您所见,JournalEntry
返回一个逗号分隔的实体属性字符串。 因为允许JournalEntry
属性为nil
,所以函数使用nil
合并运算符(??
) 导出一个空字符串,而不是属性为nil
的无用调试消息。
Note
nil
合并运算符(??
) 如果包含值,则展开可选项; 否则返回默认值。 例如:let coalescedHeight = height != nil ? height! : ""
可以使用nil
合并运算符缩短为:let coalescedHeight = height ?? ""
.
这就是应用程序为单个日志条目创建CSV
字符串的方式,但是应用程序如何将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
)
逐步查看CSV
导出代码:
- 首先,通过执行一个
fetch
请求来检索所有的JournalEntry
实体。 获取请求与获取结果控制器使用的请求相同。 因此,您可以重用surfJournalFetchRequest
方法来创建请求,以避免重复。 - 接下来,通过附加文件名 export.csv为导出的
CSV
文件创建URL
到NSTemporaryDirectory
方法的输出。NSTemporaryDirectory
返回的路径是临时文件存储的唯一目录。 这是一个很好的地方,文件可以很容易地再次生成,不需要备份的iTunes
或iCloud
。
创建导出URL后,调用createFile(atPath:contents:attributes:)
创建空文件,用于存储导出的数据。 如果指定的文件路径中已存在文件,此方法将首先删除该文件。
一旦应用程序有了空文件,它就可以将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()
}
下面是文件处理的工作原理:
- 首先,应用程序需要创建一个用于写入的文件处理程序,它只是一个处理写入数据所需的低级磁盘操作的对象。 要创建用于写入的文件处理程序,请使用
FileHandle(forWritingTo:)
初始化器。 - 接下来,迭代所有的
JournalEntry
实体。 在每次迭代期间,您尝试使用JournalEntry
上的csv()
和String
上的data(using:allowLossyConversion:)
创建UTF8编码的字符串。 如果成功,则使用文件处理程序write()
方法将UTF8字符串写入磁盘。 - 最后,关闭导出文件写入文件处理程序,因为不再需要它。
应用程序将所有数据写入磁盘后,将显示一个包含导出文件路径的警报对话框。
Note
这个带有导出路径的alert
控制器对于学习目的来说很好,但是对于一个真实的的应用程序,你需要为用户提供一种检索导出的CSV
文件的方法,例如使用UIActivityViewController
。
要打开导出的CSV
文件,请使用Excel
、Numbers
或您喜爱的文本编辑器导航到并打开警报对话框中指定的文件。 如果您在Numbers
中打开该文件,您将看到以下内容:
现在,您已经了解了应用程序当前如何导出数据,现在是时候进行一些改进了。
后台导出¶
您希望UI在导出过程中继续工作。 要修复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)")
}
如前所述,这段代码通过在托管对象上下文中调用fetch()
来检索所有日志条目。
接下来,用下面的代码替换上面的代码:
// 1
coreDataStack.storeContainer.performBackgroundTask { context in
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
}
}
您现在调用堆栈的持久性存储容器上的performBackgroundTask(_:)
,而不是使用UI也使用的主托管对象上下文。 这将创建一个新的托管对象上下文并将其传递到闭包中。
performBackgroundTask(_:)
创建的context
在私有队列上,不会阻塞主UI队列。 闭包中的代码在该私有队列上运行。
您还可以手动创建一个新的并发类型为的临时私有上下文。privateQueueConcurrencyType
而不是使用performBackgroundTask(_:)
。
Note
托管对象上下文可以使用两种并发类型:
Private Queue 指定将与专用调度队列而不是主队列关联的上下文。 这是您刚才用来将导出操作移出主队列的队列类型,这样它就不会再干扰UI
。
Main Queue 默认类型,上下文关联到主队列。 此类型是主上下文(coreDataStack.mainContext
)使用。 任何UI操作,比如为表视图创建获取的结果控制器,都必须使用这种类型的上下文。
上下文及其托管对象必须 only 从正确的队列访问。 NSManagedObjectContext
具有perform(_:)
和performAndWait(_:)
,以将工作引导到正确的队列。 您可以添加启动参数-com。apple.CoreData.ConcurrencyDebug 1
添加到应用程序的方案中,以捕获调试器中的错误。
接下来,在同一方法中找到以下代码:
print("Export Path: \(exportFilePath)")
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
} else {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
将代码替换为以下内容:
print("Export Path: \(exportFilePath)")
// 6
DispatchQueue.main.async {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
}
} else {
DispatchQueue.main.async {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
}
} // 7 Closing brace for performBackgroundTask
要完成任务:
- 您应该始终在主队列上执行与UI相关的所有操作,例如在导出操作完成时显示警报视图;否则,可能会发生不可预知的事情。 使用
DispatchQueue。main.async
显示主队列上的最终警报视图消息。 - 最后,添加一个右花括号来关闭您在步骤1中通过
performBackgroundTask(_:)
调用打开的块。
现在,您已经将导出操作移动到了一个带有私有队列的新上下文中,现在构建并运行它,看看它是否有效!
你应该看到你之前看到的:
点击左上角的“导出”按钮,然后立即尝试滚动冲浪会话日志条目列表。 注意到这次有什么不同吗? 导出操作仍需要几秒钟才能完成,但在此期间表视图将继续滚动。 导出操作不再阻止UI
。
酷啊哥们! 这是一项让UI
更灵敏的工作。
您刚刚看到了在私有后台队列上工作如何改善用户对应用的体验。现在,您将通过检查子上下文来扩展多个上下文的使用。
在便签本上编辑¶
现在,SurfJournal
使用主上下文(coreDataStack.mainContext
)。 这种方法没有错;启动器项目按原样工作。
对于像这样的日志风格的应用程序,您可以通过将编辑或新条目视为一组更改来简化应用程序架构,就像便笺簿一样。 当用户编辑日记条目时,您将更新托管对象的属性。
一旦更改完成,您可以保存它们或丢弃它们,这取决于用户想要做什么。
您可以将子托管对象上下文视为临时便笺簿,您可以完全丢弃该便笺簿,也可以保存更改并将其发送到父上下文。
但是,从技术上讲,什么是子上下文?
所有托管对象上下文都有一个父存储区,您可以从其中检索和更改托管对象形式的数据,例如此项目中的JournalEntry
对象。 通常,父存储是持久存储协调器,这是CoreDataStack
类提供的主上下文的情况。 或者,可以将给定上下文的父存储区设置为另一个托管对象上下文,使其成为子上下文。
保存子上下文时,更改仅转到父上下文。 在保存父上下文之前,对父上下文的更改不会发送到持久性存储协调器。
在开始添加子上下文之前,您需要了解当前查看和编辑操作的工作方式。
查看编辑¶
操作的第一部分需要从主列表视图切换到日记帐详细视图。
打开 JournalListViewController.swift 并找到prepare(for:sender:)
:
// 1
if segue.identifier == "SegueListToDetail" {
// 2
guard let navigationController =
segue.destination as? UINavigationController,
let detailViewController =
navigationController.topViewController
as? JournalEntryViewController,
let indexPath = tableView.indexPathForSelectedRow else {
fatalError("Application storyboard mis-configuration")
}
// 3
let surfJournalEntry =
fetchedResultsController.object(at: indexPath)
// 4
detailViewController.journalEntry = surfJournalEntry
detailViewController.context =
surfJournalEntry.managedObjectContext
detailViewController.delegate = self
逐步执行segue
代码:
- 这里有两个
segues
:SegueListToDetail 和 SegueListToDetailAdd。 第一个代码块如前面的代码块所示,当用户点击表视图中的一行以查看或编辑以前的日记条目时运行。 - 接下来,您将获得用户最终将看到的
JournalEntryViewController
的引用。 它是在导航控制器中呈现的,因此需要进行一些解包操作。 此代码还验证表视图中是否有选定的索引路径。 - 接下来,使用获取的结果控制器的
object(at:)
方法获取用户选择的JournalEntry
。 - 最后,在
JournalEntryViewController
实例上设置所有必需的变量。surfJournalEntry
变量对应于步骤3中解析的JournalEntry
实体。 上下文变量是要用于任何操作的托管对象上下文;目前,它只使用主上下文。JournalListViewController
将自身设置为JournalEntryViewController
的委托,以便在用户完成编辑操作时通知它。
SegueListToDetailAdd 类似于 SegueListToDetail,不同之处在于应用创建新的JournalEntry
实体,而不是检索现有实体。
当用户点击右上角的加号(+)按钮创建新的日记条目时,应用程序执行 SegueListToDetailAdd。
现在您已经知道这两个segue
是如何工作的,现在打开 JournalEntryViewController。swift 并查看文件顶部的JournalEntryDelegate
协议:
protocol JournalEntryDelegate: AnyObject {
func didFinish(
viewController: JournalEntryViewController,
didSave: Bool
)
}
JournalEntryDelegate
协议很短,只有一个方法:didFinish(viewController:didSave:)
。 协议要求委托实现的此方法指示用户是否完成了编辑或查看日记条目以及是否应保存任何更改。
要了解didFinish(viewController:didSave:)
的工作原理,请切换回 JournalListViewController。swift 并找到该方法:
func didFinish(
viewController: JournalEntryViewController,
didSave: Bool
) {
// 1
guard didSave,
let context = viewController.context,
context.hasChanges else {
dismiss(animated: true)
return
}
// 2
context.perform {
do {
try context.save()
} catch let error as NSError {
fatalError("Error: \(error.localizedDescription)")
}
// 3
self.coreDataStack.saveContext()
}
// 4
dismiss(animated: true)
}
依次引用每个编号的注释:
- 首先,使用
guard
语句检查didSave
参数。 如果用户点击保存按钮而不是取消按钮,则这将是true
,因此应用程序应该保存用户的数据。guard
语句还使用hasChanges
属性来检查是否有任何更改;如果什么都没有改变,就没有必要浪费时间做更多的工作。 - 接下来,将
JournalEntryViewController
上下文保存在perform(_:)
闭包中。 代码将此上下文设置为主上下文;在本例中,它有点多余,因为只有一个上下文,但这不会改变行为。 一旦您稍后将子上下文添加到工作流中,JournalEntryViewController
上下文将与主上下文不同,因此需要此代码。 如果保存失败,则调用fatalError
,使用相关错误信息中止应用。 - 然后通过
saveContext
保存主上下文,在 CoreDataStack。swift中定义,将所有编辑保存到磁盘。 - 最后,关闭
JournalEntryViewController
。
Note
如果托管对象上下文的类型是MainQueueConcurrencyType
,则您不必在perform(_:)
中包装代码,但使用它并没有什么坏处。
如果你不知道上下文的类型,就像didFinish(viewController:didSave:)
中的情况一样,最安全的方法是使用perform(_:)
,这样它就可以同时使用父上下文和子上下文。
上面的实现有一个问题--你发现了吗?
当应用程序添加新的日记条目时,它会创建一个新对象并将其添加到托管对象上下文。 如果用户点击“取消”按钮,应用程序将不会保存上下文,但新对象仍将存在。 如果用户随后添加并保存另一个条目,则被取消的对象将 * 仍然 * 存在! 除非您有耐心一直滚动到最后,否则您不会在UI中看到它,但它会显示在CSV导出的底部。
您可以通过在用户取消视图控制器时删除对象来解决此问题。 但是,如果更改很复杂,涉及多个对象,或者需要在编辑工作流中更改对象的属性,该怎么办? 使用子上下文将帮助您轻松管理这些复杂的情况。
使用子上下文进行编辑集¶
现在您已经了解了应用程序当前如何编辑和创建JournalEntry
实体,接下来将修改实现以使用子托管对象上下文作为临时暂存区。
这很容易做到-你只需要修改segues
。 打开 JournalListViewController。swift,在prepare(for:sender:)
中找到 SegueListToDetail 的如下代码:
detailViewController.journalEntry = surfJournalEntry
detailViewController.context =
surfJournalEntry.managedObjectContext
detailViewController.delegate = self
接下来,将该代码替换为以下代码:
// 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
。 在这里,您设置了一个父上下文,而不是像创建托管对象上下文时通常所做的那样设置一个持久性存储协调器。 在这里,您将parent
设置为CoreDataStack
的mainContext
。 - 接下来,使用子上下文的
object(with:)
方法检索相关的日志条目。 您必须使用object(with:)
来检索日记条目,因为托管对象特定于创建它们的上下文。 但是,objectID
值并不特定于单个上下文,因此当您需要在多个上下文中访问对象时可以使用它们。 - 最后,在
JournalEntryViewController
实例上设置所有必需的变量。 这一次,您使用childEntry
和childContext
,而不是原来的surfJournalEntry
和surfJournalEntry。managedObjectContext
。
Note
你可能想知道为什么你需要把托管对象和托管对象上下文都传递给detailViewController
,因为托管对象已经有了一个上下文变量。 这是因为托管对象只有对上下文的弱引用。 如果您不传递上下文,ARC
将从内存中删除上下文(因为没有其他东西保留它),应用程序将不会像您期望的那样运行。
构建并运行您的应用程序;它应该和以前一样工作。 在这种情况下,应用没有可见的变化是好事;用户仍然可以在行上轻敲以查看和编辑冲浪会话日志条目。
通过使用子上下文作为日志编辑的容器,您降低了应用架构的复杂性。 在单独的上下文中进行编辑时,取消或保存托管对象更改是微不足道的。
干得好,伙计! 当涉及到多个托管对象上下文时,您不再是一个怪人。 大胆!
挑战¶
利用新学到的知识,尝试更新 SegueListToDetailAdd,以便在添加新日记条目时使用子上下文。
就像前面一样,您需要创建一个子上下文,它将主上下文作为其父上下文。 您还需要记住在正确的上下文中创建新条目。
如果你被卡住了,在本章的文件夹中查看带有挑战解决方案的项目-但首先给予你最大的努力!
关键点¶
- 托管对象上下文是用于处理托管对象的内存中暂存器。
- 可以使用私有背景上下文来防止阻塞主
UI
。 - 上下文与特定的队列相关联,只能在这些队列上访问。
- 子上下文可以简化应用的架构,使保存或丢弃编辑变得容易。
- 托管对象与它们的上下文紧密绑定,不能与其他上下文一起使用。
- 冲浪者说话很有趣。