Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Core Data Tutorial: Multiple Managed Object Contexts

$
0
0
Note from Ray: This is an abbreviated version of a chapter from Core Data by Tutorials Second Edition to give you a sneak peek of what’s inside the book, released as part of the iOS 9 Feast. This tutorial is fully up-to-date for iOS 9, Xcode 7, and Swift 2. We hope you enjoy!

A managed object context is an in-memory scratchpad that you use to work with your managed objects.

Most apps need but a single managed object context. A single managed object context with a main queue, the default behavior, is simple to manage and understand. Apps with multiple managed object contexts are harder to debug. For that reason, you should avoid them, if possible.

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, causing the UI to stutter.

In other situations, such as when temporarily editing user data, it’s helpful to treat a managed object context as a set of changes that the app can just throw away if it no longer needs them. Using child contexts makes this possible.

In this Core Data 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 Surf Journal

Open the SurfJournal starter project, then build and run the app.

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

screenshot01 screenshot02

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, letting you make changes 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, 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:

screenshot03

Before jumping into the code, let’s briefly 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”: the context, the model, the persistent store and the persistent store coordinator. 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 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 is an NSManagedObject subclass with six properties for attributes: date, height, location, period, rating and wind. It also includes the CSV export function csv. If you’re curious about this class’s entity definition, head over to SurfJournalModel.xcdatamodel.

screenshot04

When you first launched the app, it already had a significant amount of data. While it is common to import seed data from a JSON file, this sample project comes with a seeded Core Data database. Let’s see how it works.

The Core Data Stack

Open CoreDataStack.swift and find the following code:

// 1
let bundle = NSBundle.mainBundle()
let seededDatabaseURL = bundle
  .URLForResource(self.seedName, withExtension: "sqlite")!
 
// 2
let didCopyDatabase: Bool
do {
  try NSFileManager.defaultManager()
    .copyItemAtURL(seededDatabaseURL, toURL: url)
  didCopyDatabase = true
} catch {
  didCopyDatabase = false
}
 
// 3
if didCopyDatabase {

Let’s go through the code step by step:

  1. The app bundle comes with a pre-populated Core Data database named SurfJournalDatabase.sqlite. To make use of this database, first you have to find it and create a URL reference to it using URLForResource(_:withExtension:).
  2. copyItemAtURL(_:toURL:error:) attempts to copy the seeded database file to the app’s documents directory. If the database file already exists in the documents directory, the copy operation fails. This behavior allows the seeding operation to happen only once, on first launch.
  3. On subsequent app launches, the database will already exist and the copy will fail. When the copy operation fails, the variable didCopyDatabase will be false and the code in the if-statement will never execute.

Assume that the app is launching for the first time and therefore didCopyDatabase is true. Let’s see how the rest of the seeding operation works:

// 4
let seededSHMURL = bundle
  .URLForResource(self.seedName, withExtension: "sqlite-shm")!
let shmURL = self.applicationDocumentsDirectory
  .URLByAppendingPathComponent(self.seedName + ".sqlite-shm")
do {
  try NSFileManager.defaultManager()
    .copyItemAtURL(seededSHMURL, toURL: shmURL)
} catch {
  let nserror = error as NSError
  print("Error: \(nserror.localizedDescription)")
  abort()
}
 
// 5
let seededWALURL = bundle
  .URLForResource(self.seedName, withExtension: "sqlite-wal")!
let walURL = self.applicationDocumentsDirectory
  .URLByAppendingPathComponent(self.seedName + ".sqlite-wal")
do {
  try NSFileManager.defaultManager()
    .copyItemAtURL(seededWALURL, toURL: walURL)
} catch {
  let nserror = error as NSError
  print("Error: \(nserror.localizedDescription)")
  abort()
}

To support concurrent reads and writes, SQLite, the persistent store in use by this sample app, utilizes 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 that they exist 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 will be missing data.

  1. Once SurfJournalDatabase.sqlite has been successfully copied, the support file SurfJournalDatabase.sqlite-shm is copied over.
  2. Finally, the remaining support file SurfJournalDatabase.sqlite-wal, is copied over.

The only reason SurfJournalDatabase.sqlite, SurfJournalDatabase.sqlite-shm or SurfJournalDatabase.sqlite-wal would fail to copy over 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 app won’t work, there’s no point in continuing, so the initializer calls abort().

Note: We developers often frown upon using abort, as it confuses users by causing the app to quit suddenly and without explanation. But this is one example where abort is acceptable, since the app needs Core Data to work.

If an app requires Core Data to be useful 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 abort at 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 abort.

Once the pre-populated database and support files are copied over, the final step is to add the seeded database store to the persistent store coordinator.

// 6
do {
  try coordinator.addPersistentStoreWithType(
    NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
} catch {
  // 7
    let nserror = error as NSError
    print("Error: \(nserror.localizedDescription)")
    abort()
}
  1. addPersistentStoreWithType(_:configuration:URL:options:) is called on the NSPersistentStoreCoordinator to add the store (NSSQLiteStoreType in this case) at the given URL.
  2. Finally, if the store wasn’t successfully created, the app won’t work so abort is called.

Now that you know something about beginning with a seeded database, let’s start learning about multiple managed object contexts by adding a second context with a private queue to the Surf Journal app.

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 will take several seconds and it will prevent 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.

How can you fix this? The traditional way would be 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 easy: just add another context for the export operation that uses a private queue rather than the main queue, so the export operation can do its work in the background. 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 Core Data entity. Open JournalEntry.swift and find csv():

func csv() -> String {
  let coalescedHeight = height ?? ""
  let coalescedPeriod = period ?? ""
  let coalescedWind = wind ?? ""
  let coalescedLocation = location ?? ""
  var coalescedRating:String
  if let rating = rating?.intValue {
    coalescedRating = String(rating)
  } else {
    coalescedRating = ""
  }
 
  return "\(stringForDate()),\(coalescedHeight)," +
    "\(coalescedPeriod),\(coalescedWind)," +
    "\(coalescedLocation),\(coalescedRating)\n"
}

As you can see, this JournalEntry function 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 (??) so that it exports 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 by using the nil coalescing operator:

let coalescedHeight = height ?? ""

Now that you know how the app creates the CSV strings for an individual journal entry, take a look at how the app saves the CSV file to disk. Switch to JournalListViewController.swift and find the following code in exportCSVFile:

// 1
let results: [AnyObject]
do {
  results = try coreDataStack.context.executeFetchRequest(
        self.surfJournalFetchRequest())
} catch {
  let nserror = error as NSError
  print("ERROR: \(nserror)")
  results = []
}
 
// 2
let exportFilePath =
  NSTemporaryDirectory() + "export.csv"
let exportFileURL = NSURL(fileURLWithPath: exportFilePath)
NSFileManager.defaultManager().createFileAtPath(
  exportFilePath, contents: NSData(), attributes: nil)

Let’s go through the CSV export code step by step:

  1. First, the code retrieves all JournalEntry entities by executing a fetch request. The fetch request is the same one used by the fetched results controller and therefore the code uses surfJournalFetchRequest to create it, avoiding duplication.
  2. The code creates the URL for the exported CSV file by appending the file name (“export.csv”) to the output of NSTemporaryDirectory. 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, the code calls createFileAtPath(_:contents:attributes:) to create the empty file to store the exported data. If a file already exists at the specified file path, then the code removes it first.

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

// 3
let fileHandle: NSFileHandle?
do {
  fileHandle = try NSFileHandle(forWritingToURL: exportFileURL)
} catch {
  let nserror = error as NSError
  print("ERROR: \(nserror)")
  fileHandle = nil
}
 
if let fileHandle = fileHandle {
  // 4
  for object in results {
    let journalEntry = object as! JournalEntry
 
    fileHandle.seekToEndOfFile()
    let csvData = journalEntry.csv().dataUsingEncoding(
      NSUTF8StringEncoding, allowLossyConversion: false)
    fileHandle.writeData(csvData!)
  }
 
  // 5
  fileHandle.closeFile()
  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, the code calls fileHandleForWritingToURL(_:error:).
  2. Using a for-in statement, the code iterates over all JournalEntry entities. During each iteration, the code creates a UTF8-encoded string using csv and dataUsingEncoding(_:allowLossyConversion:). It then writes the UTF8 string to disk using writeData.
  3. Finally, the code closes 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:

screenshot05

Note: This alert view 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. Attaching the export file to an email is a popular method.

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:

screenshot06

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

Exporting on a Private Queue

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

Open JournalListViewController.swift and find the following code in exportCSVFile:

// 1
let results: [AnyObject]
do {
  results = try coreDataStack.context.executeFetchRequest(
        self.surfJournalFetchRequest())
} catch {
  let nserror = error as NSError
  print("ERROR: \(nserror)")
  results = []
}

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

Now replace it with the following:

// 1
let privateContext = NSManagedObjectContext(
  concurrencyType: .PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator =
  coreDataStack.context.persistentStoreCoordinator
 
// 2
privateContext.performBlock { () -> Void in
  // 3
  let results: [AnyObject]
  do {
    results = try self.coreDataStack.context
      .executeFetchRequest(self.surfJournalFetchRequest())
  } catch {
    let nserror = error as NSError
    print("ERROR: \(nserror)")
    results = []
  }

Let’s go through the new code, which utilizes a new managed object context, step by step:

  1. First, you create a new managed object context called privateContext with a concurrency type of PrivateQueueConcurrencyType, which specifies that the context will be associated with a private dispatch queue. Once you’ve created the new context, you assign it the same persistent store coordinator as the main managed object context.
  2. Next, you call performBlock. This function asynchronously performs the given block on the context’s queue. In this case, the queue is private.
  3. Just as before, you retrieve all JournalEntry entities by executing a fetch request. But this time, you use the private context to execute the fetch request.

Next, find the following code in the same function:

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

Now replace it with the following:

    // 4
    dispatch_async(dispatch_get_main_queue(), { () -> Void in
      self.navigationItem.leftBarButtonItem =
        self.exportBarButtonItem()
      print("Export Path: \(exportFilePath)")
      self.showExportFinishedAlertView(exportFilePath)
    })
  } else {
    dispatch_async(dispatch_get_main_queue(), { () -> Void in
      self.navigationItem.leftBarButtonItem =
        self.exportBarButtonItem()
    })
  }    
 
} // 5 closing brace for performBlock()
  1. You should always perform all operations related to the UI, such as showing an alert view when the export operation is finished, on the main queue; otherwise unpredictable things will happen. You use the dispatch_async and dispatch_get_main_queue to show the final alert view message on the main queue.
  2. Finally, the block you opened earlier in step 2 via the performBlock call now needs to be closed with a closing curly brace.

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

ConfinementConcurrencyType specifies that the context will use the thread confinement pattern and that the developer will be responsible for managing all thread access. You should consider this type deprecated and never use it, as the next two types will cover all use cases.

PrivateQueueConcurrencyType specifies that the context 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 that it no longer interferes with the UI.

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

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

You should see exactly what you saw before:

screenshot07

Tap the Export button in the top-left and then 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 now 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 creating a new managed object context with a private queue can improve a user’s experience with your app.

Where To Go From Here?

Here is the SurfJournal final project for this tutorial.

If you followed this Core Data tutorial all the way through, you’ve turned an app with a single managed object context into an app with multiple contexts. You improved UI responsiveness by performing the export operation on a managed object context with a private queue.

If you want to learn more about multiple managed object contexts, check out the full chapter in Core Data by Tutorials, where I go a bit further and cover parent and child contexts.

In the meantime, if you have any questions or comments, please join the forum discussion below!

The post Core Data Tutorial: Multiple Managed Object Contexts appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles