跳转至

9.多个托管对象上下文

托管对象上下文是用于处理托管对象的内存中暂存器。 在第3章“核心数据栈”中,您学习了托管对象上下文如何与核心数据栈中的其他类相适应。

大多数应用程序只需要一个托管对象上下文。 大多数Core Data应用程序中的默认配置是与主队列关联的单个托管对象上下文。 多个托管对象上下文使您的应用程序更难调试;这不是你在每一个应用程序中,在每一种情况下都会用到的东西。

话虽如此,在某些情况下确实需要使用多个托管对象上下文。 例如,长时间运行的任务(如导出数据)将阻塞仅使用单个主队列托管对象上下文的应用程序的主线程,并导致UI卡顿。

在其他情况下,例如对用户数据进行编辑时,将托管对象上下文视为一组更改会很有帮助,应用可以在不再需要这些更改时丢弃这些更改。 使用子上下文使这成为可能。

在本章中,您将通过为冲浪者提供一个日志应用程序并通过添加多个上下文以多种方式对其进行改进来了解多个托管对象上下文。

Note

如果常见的核心数据短语,如托管对象子类和持久化存储协调器没有任何铃声,或者如果你不确定核心数据堆栈应该做什么,你可能想在继续之前阅读或重读本书的前三章。 本章介绍高级主题,并假定您已经了解基本知识。

入门

这一章的入门项目是一个简单的日志应用程序的冲浪。 在每次冲浪后,冲浪者可以使用该应用程序创建一个新的日志条目,记录海洋参数,如涌浪高度或周期,并从1到5对会话进行评级。 伙计,如果你不喜欢吊十个和获得桶,不用担心,兄弟。 只要用你最喜欢的爱好来代替冲浪术语!

SurfJournal简介

转到本章的文件并找到 SurfJournal的·项目。 打开项目,然后生成并运行应用程序。

启动时,应用程序会列出所有以前的冲浪会话日志条目。 点击行将显示具有编辑功能的冲浪会话的详细视图。

正如您所看到的,示例应用程序可以正常工作并具有数据。 点击左上角的导出按钮,将数据导出到逗号分隔值(·)文件。 点击右上角的加号(+)按钮可添加新的日记条目。 点击列表中的一行可在编辑模式下打开条目,您可以在其中更改或查看冲浪会话的详细信息。

尽管示例项目看起来很简单,但它实际上做了很多事情,可以作为添加多上下文支持的良好基础。 首先,让我们确保您对项目中的各个类有一个很好的理解。

打开项目导航器,查看启动项目中的完整文件列表:

img

在开始编写代码之前,先花一点时间回顾一下每个类的开箱即用功能。 如果您已经完成了前面的章节,您应该会发现这些类中的大多数都很熟悉:

  • AppDelegate:在第一次启动时,应用委托创建Core Data堆栈并设置主视图控制器JournalListViewController上的coreDataStack属性。
  • CoreDataStack:和前面章节一样,这个对象包含核心数据对象,称为 stack 。 与前几章不同的是,这一次堆栈安装的数据库在第一次启动时就已经有了数据。 现在还不用担心这个;你很快就会看到它是如何工作的。
  • JournalListViewController:示例项目是一个单页的基于表的应用程序。 此文件代表该表。 如果你对它的UI元素感兴趣,请前往 Main.storyboard 。 有一个嵌入在导航控制器中的表视图控制器和一个类型为 SurfEntryTableViewCell 的单个原型单元。
  • JournalEntryViewController:此类处理创建和编辑冲浪日志条目。 您可以在 Main.storyboard 中看到它的界面。
  • JournalEntry:此类表示冲浪日志条目。 它是一个NSManagedObject子类,有六个属性:dateheightlocationperiodratingwind。 如果你对这个类的实体定义感兴趣,请查看 SurfJournalModel。xcdatamodel

img

  • 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 版本有点不同:

  1. 首先检查UserDefaults以获取previouslyLaunched布尔值。 如果当前执行确实是应用程序的第一次启动,则Bool将为false,使if语句为true。 在第一次启动时,您要做的第一件事是将previouslyLaunched设置为true,这样播种操作就不会再次发生。
  2. 然后复制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")
}
  1. 一次 SurfJournalModel的副本。sqlite 已成功,然后复制支持文件 SurfJournalModel。sqlite-shm.
  2. 最后,复制剩余的支持文件 SurfJournalModel。sqlite-wal.

唯一的原因是 SurfJournalModel.sqliteSurfJournalModel.sqlite-shmSurfJournalModel.sqlite-wal 在第一次启动时复制失败是因为发生了一些非常糟糕的事情,例如宇宙辐射导致的磁盘损坏。 在这种情况下,设备,包括任何应用程序,也可能会失败。 如果文件复制失败,继续复制就没有意义了,所以catch块调用fatalError

Note

开发人员通常不喜欢使用abortfatalError,因为它会导致应用程序突然退出而没有解释,从而使用户感到困惑。 这是一个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导出代码:

  1. 首先,通过执行一个fetch请求来检索所有的JournalEntry实体。 获取请求与获取结果控制器使用的请求相同。 因此,您可以重用surfJournalFetchRequest方法来创建请求,以避免重复。
  2. 接下来,通过附加文件名 export.csv为导出的CSV文件创建URLNSTemporaryDirectory方法的输出。 NSTemporaryDirectory返回的路径是临时文件存储的唯一目录。 这是一个很好的地方,文件可以很容易地再次生成,不需要备份的iTunesiCloud

创建导出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()
}

下面是文件处理的工作原理:

  1. 首先,应用程序需要创建一个用于写入的文件处理程序,它只是一个处理写入数据所需的低级磁盘操作的对象。 要创建用于写入的文件处理程序,请使用FileHandle(forWritingTo:)初始化器。
  2. 接下来,迭代所有的JournalEntry实体。 在每次迭代期间,您尝试使用JournalEntry上的csv()String上的data(using:allowLossyConversion:)创建UTF8编码的字符串。 如果成功,则使用文件处理程序write()方法将UTF8字符串写入磁盘。
  3. 最后,关闭导出文件写入文件处理程序,因为不再需要它。

应用程序将所有数据写入磁盘后,将显示一个包含导出文件路径的警报对话框。

img

Note

这个带有导出路径的alert控制器对于学习目的来说很好,但是对于一个真实的的应用程序,你需要为用户提供一种检索导出的CSV文件的方法,例如使用UIActivityViewController

要打开导出的CSV文件,请使用ExcelNumbers或您喜爱的文本编辑器导航到并打开警报对话框中指定的文件。 如果您在Numbers中打开该文件,您将看到以下内容:

img

现在,您已经了解了应用程序当前如何导出数据,现在是时候进行一些改进了。

后台导出

您希望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 指定将与专用调度队列而不是主队列关联的上下文。 这是您刚才用来将导出操作移出主队列的队列类型,这样它就不会再干扰UIMain 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

要完成任务:

  1. 您应该始终在主队列上执行与UI相关的所有操作,例如在导出操作完成时显示警报视图;否则,可能会发生不可预知的事情。 使用DispatchQueue。main.async显示主队列上的最终警报视图消息。
  2. 最后,添加一个右花括号来关闭您在步骤1中通过performBackgroundTask(_:)调用打开的块。

现在,您已经将导出操作移动到了一个带有私有队列的新上下文中,现在构建并运行它,看看它是否有效!

你应该看到你之前看到的:

img

点击左上角的“导出”按钮,然后立即尝试滚动冲浪会话日志条目列表。 注意到这次有什么不同吗? 导出操作仍需要几秒钟才能完成,但在此期间表视图将继续滚动。 导出操作不再阻止UI

酷啊哥们! 这是一项让UI更灵敏的工作。

您刚刚看到了在私有后台队列上工作如何改善用户对应用的体验。现在,您将通过检查子上下文来扩展多个上下文的使用。

在便签本上编辑

现在,SurfJournal使用主上下文(coreDataStack.mainContext)。 这种方法没有错;启动器项目按原样工作。

对于像这样的日志风格的应用程序,您可以通过将编辑或新条目视为一组更改来简化应用程序架构,就像便笺簿一样。 当用户编辑日记条目时,您将更新托管对象的属性。

一旦更改完成,您可以保存它们或丢弃它们,这取决于用户想要做什么。

您可以将子托管对象上下文视为临时便笺簿,您可以完全丢弃该便笺簿,也可以保存更改并将其发送到父上下文。

但是,从技术上讲,什么是子上下文?

所有托管对象上下文都有一个父存储区,您可以从其中检索和更改托管对象形式的数据,例如此项目中的JournalEntry对象。 通常,父存储是持久存储协调器,这是CoreDataStack类提供的主上下文的情况。 或者,可以将给定上下文的父存储区设置为另一个托管对象上下文,使其成为子上下文。

img

保存子上下文时,更改仅转到父上下文。 在保存父上下文之前,对父上下文的更改不会发送到持久性存储协调器。

在开始添加子上下文之前,您需要了解当前查看和编辑操作的工作方式。

查看编辑

操作的第一部分需要从主列表视图切换到日记帐详细视图。

打开 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代码:

  1. 这里有两个seguesSegueListToDetailSegueListToDetailAdd。 第一个代码块如前面的代码块所示,当用户点击表视图中的一行以查看或编辑以前的日记条目时运行。
  2. 接下来,您将获得用户最终将看到的JournalEntryViewController的引用。 它是在导航控制器中呈现的,因此需要进行一些解包操作。 此代码还验证表视图中是否有选定的索引路径。
  3. 接下来,使用获取的结果控制器的object(at:)方法获取用户选择的JournalEntry
  4. 最后,在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)
}

依次引用每个编号的注释:

  1. 首先,使用guard语句检查didSave参数。 如果用户点击保存按钮而不是取消按钮,则这将是true,因此应用程序应该保存用户的数据。 guard语句还使用hasChanges属性来检查是否有任何更改;如果什么都没有改变,就没有必要浪费时间做更多的工作。
  2. 接下来,将JournalEntryViewController上下文保存在perform(_:)闭包中。 代码将此上下文设置为主上下文;在本例中,它有点多余,因为只有一个上下文,但这不会改变行为。 一旦您稍后将子上下文添加到工作流中,JournalEntryViewController上下文将与主上下文不同,因此需要此代码。 如果保存失败,则调用fatalError,使用相关错误信息中止应用。
  3. 然后通过saveContext保存主上下文,在 CoreDataStack。swift中定义,将所有编辑保存到磁盘。
  4. 最后,关闭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

以下是具体的情况:

  1. 首先,使用创建一个名为childContext的新托管对象上下文。mainQueueConcurrencyType。 在这里,您设置了一个父上下文,而不是像创建托管对象上下文时通常所做的那样设置一个持久性存储协调器。 在这里,您将parent设置为CoreDataStackmainContext
  2. 接下来,使用子上下文的object(with:)方法检索相关的日志条目。 您必须使用object(with:)来检索日记条目,因为托管对象特定于创建它们的上下文。 但是,objectID值并不特定于单个上下文,因此当您需要在多个上下文中访问对象时可以使用它们。
  3. 最后,在JournalEntryViewController实例上设置所有必需的变量。 这一次,您使用childEntrychildContext,而不是原来的surfJournalEntrysurfJournalEntry。managedObjectContext

Note

你可能想知道为什么你需要把托管对象和托管对象上下文都传递给detailViewController,因为托管对象已经有了一个上下文变量。 这是因为托管对象只有对上下文的弱引用。 如果您不传递上下文,ARC将从内存中删除上下文(因为没有其他东西保留它),应用程序将不会像您期望的那样运行。

构建并运行您的应用程序;它应该和以前一样工作。 在这种情况下,应用没有可见的变化是好事;用户仍然可以在行上轻敲以查看和编辑冲浪会话日志条目。

img

通过使用子上下文作为日志编辑的容器,您降低了应用架构的复杂性。 在单独的上下文中进行编辑时,取消或保存托管对象更改是微不足道的。

干得好,伙计! 当涉及到多个托管对象上下文时,您不再是一个怪人。 大胆!

挑战

利用新学到的知识,尝试更新 SegueListToDetailAdd,以便在添加新日记条目时使用子上下文。

就像前面一样,您需要创建一个子上下文,它将主上下文作为其父上下文。 您还需要记住在正确的上下文中创建新条目。

如果你被卡住了,在本章的文件夹中查看带有挑战解决方案的项目-但首先给予你最大的努力!

关键点

  • 托管对象上下文是用于处理托管对象的内存中暂存器。
  • 可以使用私有背景上下文来防止阻塞主UI
  • 上下文与特定的队列相关联,只能在这些队列上访问。
  • 子上下文可以简化应用的架构,使保存或丢弃编辑变得容易。
  • 托管对象与它们的上下文紧密绑定,不能与其他上下文一起使用。
  • 冲浪者说话很有趣。