Multiple Managed Object Contexts with Core Data Tutorial

Learn how to use multiple managed object contexts to improve the performance of your apps in this Core Data Tutorial in Swift! By Matthew Morey.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Editing on a Scratchpad

Right now, SurfJournal uses the main context (coreDataStack.mainContext) when creating a new journal entry or viewing an existing one. There’s nothing wrong with this approach; the starter project works as-is.

For journaling-style apps like this one, you can simplify the app architecture by thinking of edits or new entries as a set of changes, like a scratch pad. As the user edits the journal entry, you update the attributes of the managed object. Once the changes are complete, you either save them or throw them away, depending on what the user wants to do.

You can think of child managed object contexts as temporary scratch pads that you can either discard completely, or save and send the changes to the parent context.

But what is a child context, technically?

All managed object contexts have a parent store from which you can retrieve and change data in the form of managed objects, such as the JournalEntry objects in this project. Typically, the parent store is a persistent store coordinator, which is the case for the main context provided by the CoreDataStack class. Alternatively, you can set the parent store for a given context to another managed object context, making it a child context.

When you save a child context, the changes only go to the parent context. Changes to the parent context won’t be sent to the persistent store coordinator until the parent context is saved.

Before you jump in and add a child context, you need to understand how the current viewing and editing operation works.

Viewing and Editing

The first part of the operation requires segueing from the main list view to the journal detail view. Open JournalListViewController.swift and find 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

Taking the segue code step-by-step:

  1. There are two segues: SegueListToDetail and SegueListToDetailAdd. The first, shown in the previous code block, runs when the user taps on a row in the table view to view or edit a previous journal entry.
  2. Next, you get a reference to the JournalEntryViewController the user is going to end up seeing. It’s presented inside a navigation controller so there’s some unpacking to do. This code also verifies that there’s a selected index path in the table view.
  3. Next, you get the JournalEntry selected by the user, using the fetched results controller’s object(at:) method.
  4. Finally, you set all required variables on the JournalEntryViewController instance. The surfJournalEntry variable corresponds to the JournalEntry entity resolved in step 3. The context variable is the managed object context to be used for any operation; for now, it just uses the main context. The JournalListViewController sets itself as the delegate of the JournalEntryViewController so it can be informed when the user has completed the edit operation.

SegueListToDetailAdd is similar to SegueListToDetail, except the app creates a new JournalEntry entity instead of retrieving an existing one. The app executes SegueListToDetailAdd when the user taps the plus (+) button on the top-right to create a new journal entry.

Now that you know how both segues work, open JournalEntryViewController.swift and look at the JournalEntryDelegate protocol at the top of the file:

protocol JournalEntryDelegate {
  func didFinish(viewController: JournalEntryViewController,
                 didSave: Bool)
}

The JournalEntryDelegate protocol is very short and consists of only one method: didFinish(viewController:didSave:). This method, which the protocol requires the delegate to implement, indicates if the user is done editing or viewing a journal entry and whether any changes should be saved.

To understand how didFinish(viewController:didSave:) works, switch back to JournalListViewController.swift and find that method:

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)
}

Taking each numbered comment in turn:

Once you add a child context to the workflow later on, the JournalEntryViewController context will be different from the main context, making this code necessary.

If the save fails, call fatalError to abort the app with the relevant error information.

  1. First, use a guard statement to check the didSave parameter. This will be true if the user taps the Save button instead of the Cancel button, so the app should save the user’s data. The guard statement also uses the hasChanges property to check if anything’s changed; if nothing has changed, there’s no need to waste time doing more work.
  2. Next, save the JournalEntryViewController context inside of a perform(_:) closure. The code sets this context to the main context; in this case it’s a bit redundant since there’s only one context, but this doesn’t change the behavior.

    Once you add a child context to the workflow later on, the JournalEntryViewController context will be different from the main context, making this code necessary.

    If the save fails, call fatalError to abort the app with the relevant error information.

  3. Next, save the main context via saveContext, defined in CoreDataStack.swift, persisting any edits to disk.
  4. Finally, dismiss the JournalEntryViewController.

If you don’t know what type the context will be, as is the case in didFinish(viewController:didSave:), it’s safest to use perform(_:) so it will work with both parent and child contexts.

Note: If a managed object context is of type MainQueueConcurrencyType, you don’t have to wrap code in perform(_:), but it doesn’t hurt to use it.

If you don’t know what type the context will be, as is the case in didFinish(viewController:didSave:), it’s safest to use perform(_:) so it will work with both parent and child contexts.

There’s a problem with the above implementation — have you spotted it?

When the app adds a new journal entry, it creates a new object and adds it to the managed object context. If the user taps the Cancel button, the app won’t save the context, but the new object will still be present. If the user then adds and saves another entry, the canceled object will still be present! You won’t see it in the UI unless you’ve got the patience to scroll all the way to the end, but it will show up at the bottom of the CSV export.

You could solve this problem by deleting the object when the user cancels the view controller. But what if the changes were complex, involved multiple objects, or required you to alter properties of an object as part of the editing workflow? Using a child context will help you manage these complex situations with ease.