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

This is an abridged chapter from our book Core Data by Tutorials, which has been completely updated for Swift 4.2 and iOS 12. This tutorial is presented as part of our iOS 12 Launch Party — enjoy!

A managed object context is an in-memory scratchpad for working with your managed objects. Most apps need just a single managed object context. The default configuration in most Core Data apps is a single managed object context associated with the main queue. Multiple managed object contexts make your apps harder to debug; it’s not something you’d use in every app, in every situation.

That being said, certain situations do warrant the use of more than one managed object context. For example, long-running tasks, such as exporting data, will block the main thread of apps that use only a single main-queue managed object context and cause the UI to stutter.

In other situations, such as when edits are being made to user data, it’s helpful to treat a managed object context as a set of changes that the app can discard if it no longer needs them. Using child contexts makes this possible.

In this tutorial, you’ll learn about multiple managed object contexts by taking a journaling app for surfers and improving it in several ways by adding multiple contexts.

Note: This is an advanced tutorial, and assumes prior knowledge of Swift, Core Data, and iOS app development in general. If common Core Data phrases such as managed object subclass and persistent store coordinator don’t ring any bells, or if you’re unsure what a Core Data stack is supposed to do, you may want to read some of our other Core Data tutorials first.

Getting Started

This tutorial’s starter project is a simple journal app for surfers. After each surf session, a surfer can use the app to create a new journal entry that records marine parameters, such as swell height or period, and rate the session from 1 to 5. Dude, if you’re not fond of hanging ten and getting barreled, no worries, brah. Just replace the surfing terminology with your favorite hobby of choice!

Introducing SurfJournal

Download the resources for this tutorial using the “Download materials” button at the top or bottom of this page. Find the SurfJournal starter project. Open the project, then build and run the app.

On startup, the application lists all previous surf session journal entries. Tapping a row brings up the detail view of a surf session with the ability to make edits.

As you can see, the sample app works and has data. Tapping the Export button on the top-left exports the data to a comma-separated values (CSV) file. Tapping the plus (+) button on the top-right adds a new journal entry. Tapping a row in the list opens the entry in edit mode, where you can change or view the details of a surf session.

Although the sample project appears simple, it actually does a lot and will serve as a good base to add multi-context support. First, let’s make sure you have a good understanding of the various classes in the project.

Open the project navigator and take a look at the full list of files in the starter project:

Before jumping into the code, take a brief moment to go over what each class does for you out of the box.

  • AppDelegate: On first launch, the app delegate creates the Core Data stack and sets the coreDataStack property on the primary view controller JournalListViewController.
  • CoreDataStack: This object contains the cadre of Core Data objects known as the stack. This time the stack installs a database that already has data in it on first launch. No need to worry about this just yet; you’ll see how it works shortly.
  • JournalListViewController: The sample project is a one-page, table-based application. This file represents that table. If you’re curious about its UI elements, head over to Main.storyboard. There’s a table view controller embedded in a navigation controller and a single prototype cell of type SurfEntryTableViewCell.
  • JournalEntryViewController: This class handles creating and editing surf journal entries. You can see its UI in Main.storyboard.
  • JournalEntry: This class represents a surf journal entry. It’s an NSManagedObject subclass with six properties for attributes: date, height, location, period, rating and wind. If you’re curious about this class’s entity definition, check out SurfJournalModel.xcdatamodel.

  • JournalEntry+Helper: This is an extension to the JournalEntry object. It includes the CSV export method csv() and the stringForDate() helper method. These methods are implemented in the extension to avoid being destroyed when you make changes to the Core Data model.

There was already a significant amount of data when you first launched the app. This sample project comes with a seeded Core Data database.

The Core Data Stack

Open CoreDataStack.swift and find the following code in 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)")
  }  

As you can see, this tutorial’s version of CoreDataStack.swift is a little different:

  1. You first check UserDefaults for the previouslyLaunched boolean value. If the current execution is indeed the app’s first launch, the Bool will be false, making the if statement true. On first launch, the first thing you do is set previouslyLaunched to true so the seeding operation never happens again.
  2. You then copy the SQLite seed file SurfJournalModel.sqlite, included with the app bundle, to the directory returned by the Core Data-provided method NSPersistentContainer.defaultDirectoryURL().

Now view the rest of 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. Once the copy of SurfJournalModel.sqlite has succeeded, you then copy over the support file SurfJournalModel.sqlite-shm.
  2. Finally, you copy over the remaining support file SurfJournalModel.sqlite-wal.

The only reason SurfJournalModel.sqlite, SurfJournalModel.sqlite-shm or SurfJournalModel.sqlite-wal would fail to copy on first launch is if something really bad happened, such as disk corruption from cosmic radiation. In that case, the device, including any apps, would likely also fail. If the files fail to copy, there’s no point in continuing, so the catch blocks call fatalError.

Calling fatalError, at the very least, generates a stack trace, which can be helpful when trying to fix the problem. If your app has support for remote logging or crash reporting, you should log any relevant information that might be helpful for debugging before calling fatalError.

Note: Developers often frown upon using abort and fatalError, as it confuses users by causing the app to quit suddenly and without explanation. This is one scenario where fatalError is acceptable, since the app needs Core Data to work. If an app requires Core Data and Core Data isn’t working, there’s no point in letting the app continue on, only to fail sometime later in a non-deterministic way.

Calling fatalError, at the very least, generates a stack trace, which can be helpful when trying to fix the problem. If your app has support for remote logging or crash reporting, you should log any relevant information that might be helpful for debugging before calling fatalError.

To support concurrent reads and writes, the persistent SQLite store in this sample app uses SHM (shared memory file) and WAL (write-ahead logging) files. You don’t need to know how these extra files work, but you do need to be aware of their existence, and that you need to copy them over when seeding the database. If you fail to copy over these files, the app will work, but it might be missing data.

Now that you know something about beginning with a seeded database, you’ll learn about multiple managed object contexts by working on a temporary private context.