Easier Core Data Setup with Persistent Containers

Apple has made a number of changes in iOS 10 to ease the pain of setting up and using Core Data. In this post I look at how to use the new persistent container class to remove a lot of the boilerplate Core Data setup code. If you are still relying on the old Xcode template code for Core Data in your app delegate you should take a look at NSPersistentContainer.

Core Data Stack Setup - The Easy Way

Setting up a Core Data stack takes some work. A typical setup needs a number of steps:

  • Load the managed object model from the application bundle
  • Create a persistent store coordinator with the model
  • Ask the coordinator to load a persistent store
  • Create one or more managed object contexts

This is a lot of work and results in a page of setup code before you can do anything with the managed objects. Starting with iOS 10 the NSPersistentContainer removes most of this boilerplate code.

If you adopt some simple naming conventions and the default store location the setup of a core data stack can be as simple as this:

container = NSPersistentContainer(name: "storeName")
container.loadPersistentStores { (storeDescription, error) in
  if let error = error {
    fatalError("Failed to load store: \(error)")
  }
}

The Objective-C version:

container = [NSPersistentContainer persistentContainerWithName:@"storeName"];
[container loadPersistentStoresWithCompletionHandler:
 ^(NSPersistentStoreDescription *storeDescription, NSError *error) {
  if (error != nil) {
    NSLog(@"Failed to load store: %@", error);
    abort();
  }
}];

The persistent container uses the name you pass for both the managed object model (name.momd) and the name of the persistent store (name.sqlite) which it creates or loads from a default directory (Application Support).

You can also load the managed object model yourself before creating the persistent container:

guard let mom = NSManagedObjectModel.mergedModel(from: nil) else {
  fatalError("Could not load model")  
}

container = NSPersistentContainer(name: name, managedObjectModel: mom)

Persistent Store Descriptor

A persistent container collects the settings for a persistent store into a persistent store descriptor. To override the defaults create a new descriptor before loading the store. You create a persistent store descriptor with the URL for the store. You can get the default store location from the container class:

let defaultURL = NSPersistentContainer.defaultDirectoryURL()
let description = NSPersistentStoreDescription(url: defaultURL)

The default URL puts the store in the Application Support directory of the application container.

The most commonly used configuration settings:

  • type: String constant specifying the store type (default is NSSQLLiteStoreType).
  • isReadOnly: Bool set to true for a read only store (default is false).
  • shouldAddStoreAsynchronously: Bool set to true to add a store to the coordinator asynchronously on a background thread. The default is false which adds the store synchronously on the calling thread.
  • shouldInferMappingAutomatically: Bool if the flag shouldMigrateStoreAutomatically is true try to infer a mapping model when migrating. Default is true.
  • shouldMigrateStoreAutomatically: Bool migrate the store automatically. Default is true.

So to create a read-only persistent store that loads asynchronously on a background thread (and by default that migrates automatically):

let url = NSPersistentContainer.defaultDirectoryURL()
let description = NSPersistentStoreDescription(url: url)
description.shouldAddStoreAsynchronously = true
description.isReadOnly = true
container.persistentStoreDescriptions = [description]

Note that you need to set the store description before you load the store.

Getting the Main View Context

The persistent container has a convenient read-only property named viewContext to get the managed object context for the main queue. As the name suggests this is the context you should be using when working with your user interface.

container.viewContext.perform({
  // ...
})

I should note at this point that Apple tells us that this view context automatically consumes save notifications from other notifications. This removes some more boilerplate code to listen for and merge save notifications but I have found in practise that I need to set the flag automaticallyMergesChangesFromParent on the view context for it to work:

// let viewContext = container.viewContext
viewContext.automaticallyMergesChangesFromParent = true

Not sure if that is a bug or a feature.

Performing a Background Task

To avoid blocking the user interface you should not use the main view context for time consuming tasks. Create a private managed object context and execute the task in the background. The persistent container has a convenience method that takes care of creating a temporary private context for you and takes a block to execute:

container.performBackgroundTask({ (context) in 
  // ... do some task on the context

  // save the context
  do {
    try context.save()
  } catch {
    // handle error
  }
})

Getting a Private Context

You can also just get a new private context to use anyway you see fit:

let context = persistentContainer.newBackgroundContext()
context.perform({
  // ...
})

Further Reading