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 2 of 4 of this article. Click here to view the first page.

Doing Work in the Background

If you haven’t done so already, tap the Export button at the top-left and then immediately try to scroll the list of surf session journal entries. Notice anything? The export operation takes several seconds, and it prevents the UI from responding to touch events such as scrolling.

The UI is blocked during the export operation because both the export operation and UI are using the main queue to perform their work. This is the default behavior.

The traditional way to fix this is to use Grand Central Dispatch to run the export operation on a background queue. However, Core Data managed object contexts are not thread-safe. That means you can’t just dispatch to a background queue and use the same Core Data stack.

The solution is simple: use a private background queue rather than the main queue for the export operation. This will keep the main queue free for the UI to use.

But before you jump in and fix the problem, you need to understand how the export operation works.

Exporting Data

Start by viewing how the app creates the CSV strings for the JournalEntry entity. Open JournalEntry+Helper.swift and find csv():

func csv() -> String {
  let coalescedHeight = height ?? ""
  let coalescedPeriod = period ?? ""
  let coalescedWind = wind ?? ""
  let coalescedLocation = location ?? ""
  let coalescedRating: String
  if let rating = rating?.int32Value {
    coalescedRating = String(rating)
  } else {
    coalescedRating = ""
  }

  return "\(stringForDate()),\(coalescedHeight),\(coalescedPeriod),\(coalescedWind),\(coalescedLocation),\(coalescedRating)\n"
}

As you can see, JournalEntry returns a comma-separated string of the entity’s attributes. Because the JournalEntry attributes are allowed to be nil, the function uses the nil coalescing operator (??) to export an empty string instead of an unhelpful debug message that the attribute is nil.

Note: The nil coalescing operator (??) unwraps an optional if it contains a value; otherwise it returns a default value. For example, the following: let coalescedHeight = height != nil ? height! : "" can be shortened using the nil coalescing operator to: let coalescedHeight = height ?? "".

That’s how the app creates the CSV strings for an individual journal entry, but how does the app save the CSV file to disk? Open JournalListViewController.swift and find the following code in 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)

Going through the CSV export code step-by-step:

The fetch request is the same one used by the fetched results controller. Therefore, you reuse the surfJournalFetchRequest method to create the request to avoid duplication.

The path returned by NSTemporaryDirectory is a unique directory for temporary file storage. This a good place for files that can easily be generated again and don’t need to be backed up by iTunes or to iCloud.

After creating the export URL, call createFile(atPath:contents:attributes:) to create the empty file where you’ll store the exported data. If a file already exists at the specified file path, this method will remove it first.

  1. First, retrieve all JournalEntry entities by executing a fetch request.
  2. Next, create the URL for the exported CSV file by appending the file name (“export.csv”) to the output of the NSTemporaryDirectory method.

Once the app has the empty file, it can write the CSV data to disk:

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

Here’s how the file-handling works:

  1. First, the app needs to create a file handler for writing, which is simply an object that handles the low-level disk operations necessary for writing data. To create a file handler for writing, use the FileHandle(forWritingTo:) initializer.

During each iteration, you attempt to create a UTF8-encoded string using csv() on JournalEntry and data(using:allowLossyConversion:) on String.

If it’s successful, you write the UTF8 string to disk using the file handler write() method.

  1. Next, iterate over all JournalEntry entities.
  2. Finally, close the export file-writing file handler, since it’s no longer needed.

Once the app has written all the data to disk, it shows an alert dialog with the exported file path.

Note: This alert controller with the export path is fine for learning purposes, but for a real app, you’ll need to provide the user with a way to retrieve the exported CSV file, for example using UIActivityViewController.

To open the exported CSV file, use Excel, Numbers or your favorite text editor to navigate to and open the file specified in the alert dialog. If you open the file in Numbers you will see the following:

Now that you’ve seen how the app currently exports data, it’s time to make some improvements.

Exporting in the Background

You want the UI to continue working while the export is happening. To fix the UI problem, you’ll perform the export operation on a private background context instead of on the main context.

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

As you saw earlier, this code retrieves all of the journal entries by calling fetch() on the managed object context.

Next, replace the above code with the following:

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

Instead of using the main managed object context also used by the UI, you’re now calling performBackgroundTask(_:) on the stack’s persistent store container. This creates a new managed object context and passes it into the closure.

The context created by performBackgroundTask(_:) is on a private queue, which doesn’t block the main UI queue. The code in the closure is run on that private queue. You could also manually create a new temporary private context with a concurrency type of .privateQueueConcurrencyType instead of using performBackgroundTask(_:).

Private Queue specifies the context that will be associated with a private dispatch queue instead of the main queue. This is the type of queue you just used to move the export operation off of the main queue so it would no longer interfere with the UI.

Main Queue, the default type, specifies that the context will be associated with the main queue. This type is what the main context (coreDataStack.mainContext) uses. Any UI operation, such as creating the fetched results controller for the table view, must use a context of this type.

Contexts and their managed objects must only be accessed from the correct queue. NSManagedObjectContext has perform(_:) and performAndWait(_:) to direct work to the correct queue. You can add the launch argument -com.apple.CoreData.ConcurrencyDebug 1 to your app’s scheme to catch mistakes in the debugger.

Note: There are two concurrency types a managed object context can use:

Next, find the following code in the same method:

  print("Export Path: \(exportFilePath)")
  self.navigationItem.leftBarButtonItem =
    self.exportBarButtonItem()
  self.showExportFinishedAlertView(exportFilePath)
} else {
  self.navigationItem.leftBarButtonItem =
    self.exportBarButtonItem()
}

Replace the code with the following:

    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

To finish off the task:

  1. You should always perform all operations related to the UI on the main queue, such as showing an alert view when the export operation is finished; otherwise, unpredictable things might happen. Use DispatchQueue.main.async to show the final alert view message on the main queue.
  2. Finally, add a closing curly brace to close the block you opened earlier in step 1 via the performBackgroundTask(_:) call.

Now that you’ve moved the export operation to a new context with a private queue, build and run to see if it works!

You should see exactly what you saw before:

Tap the Export button in the top left, and immediately try to scroll the list of surf session journal entries. Notice anything different this time? The export operation still takes several seconds to complete, but the table view continues to scroll during this time. The export operation is no longer blocking the UI.

Cowabunga, dude! Gnarly job making the UI more responsive.

You’ve just witnessed how doing work on a private background queue can improve a user’s experience with your app. Now you’ll expand on the use of multiple contexts by examining a child context.