Building the Perfect Core Data Stack With NSPersistentContainer

In Building the Perfect Core Data Stack, I listed the requirements the perfect Core Data stack needs to meet and we implemented a Core Data stack that satisfied those requirements. The series focuses on five requirements:

  • Bring Your Own: Encapsulate the Core Data stack in a dedicated class.
  • Two Is Better Than One: Use multiple managed object contexts to optimize performance.
  • Keeping It Private: Use private child managed object contexts to perform Core Data operations in the background.
  • Passing It Around: Adopt dependency injection to pass the Core Data stack to the objects that need access to it.
  • Give It Time: Access the Core Data stack only when it is safe to do so.

In June of this year, Apple revealed an impressive list of additions and improvements to the Core Data framework. The NSPersistentContainer class is one of the additions developers have been asking for since the framework was introduced more than a decade ago. The NSPersistentContainer class encapsulates the Core Data stack. Setting up and managing a Core Data stack has never been easier.

In this tutorial, I explore how well the NSPersistentContainer class satisfies the requirements we defined for the perfect Core Data stack.

Bring Your Own

Apple has finally added a class to the Core Data framework that encapsulates the nitty-gritty details of the Core Data stack. The NSPersistentContainer class is responsible for setting up and managing the Core Data stack of your application. The NSPersistentContainer class has several features in common with the CoreDataManager class we built in Building the Perfect Core Data Stack.

The NSPersistentContainer class is in charge of loading the data model, creating a managed object model, and using it to create a persistent store coordinator. Alternatively, you can create a managed object model yourself and use it to initialize a persistent container.

If you create a new project in Xcode and check the Use Core Data checkbox during the setup of the project, the application delegate is no longer cluttered with boilerplate code for setting up the Core Data stack.

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "Stargazers")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

func saveContext () {
    let context = persistentContainer.viewContext
    if context.hasChanges {
        do {
            try context.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}

If we ignore error handling for a moment, setting up the Core Data stack can be distilled into two lines of code. The NSPersistentContainer class takes care of the nitty-gritty details of setting up and managing the Core Data stack.

let container = NSPersistentContainer(name: "Stargazers")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in })

Keeping It Private

The CoreDataManager class manages two managed object contexts:

  • a managed object context associated with the main queue
  • a managed object context associated with a private queue

Using Multiple Managed Object Context In a Core Data Stack

The main managed object context is a child managed object context of the private managed object context. The benefit is that a save operation of the main managed object context doesn't result in disk I/O on the main thread. When the private managed object context performs a save operation, the changes are pushed to the persistent store coordinator on a private queue.

How the NSPersistentContainer class handles this internally is difficult to say. Since we don't have insight into the implementation of the class, we can only make an educated guess. That said, Apple has more than a decade of experience building and optimizing the Core Data framework. Chances are that the NSPersistentContainer class uses a similar approach to make sure a save operation performed by the managed object context doesn't block the main thread.

Two Is Better Than One

For more complex applications, it is important to have the ability to perform Core Data operations in the background. For that, we need a separate managed object context that is tied to a private queue.

Remember that the CoreDataManager class has the option to spawn a managed object context for background operations. The NSPersistentContainer class has a similar feature. You can ask it for a managed object context that is tied to a private queue by invoking newBackgroundContext().

let managedObjectContext = persistentContainer.newBackgroundContext()

If you only need to perform a small Core Data operation in the background, you can hand the persistent container a chunk of work using the performBackgroundTask(_:) method. The closure this method accepts defines one parameter, the managed object context that is used for the background task. This is a very nice feature of the NSPersistentContainer class that I may add to the CoreDataManager class.

persistentContainer.performBackgroundTask { (managedObjectContext) in
    ...
}

To be clear, you can access the main managed object context, the managed object context associated with the main queue, through the viewContext property.

let managedObjectContext = persistentContainer.viewContext

Passing It Around

Even though the installment covering dependency injection isn't related or applicable to the NSPersistentContainer class, the latter introduces another benefit. Because one object, an instance of the NSPersistentContainer class, encapsulates the setup and management of the Core Data stack, it is easy to pass it around in your application using dependency injection.

In most scenarios, however, you won't have to pass the persistent container itself. You can pass around the view or main managed object context or, for background operations, spawn a private managed object context and pass that to other parts of your application.

Give It Time

In Give It Time, I talked about two possible issues when setting up the Core Data stack of your application:

  • adding a persistent store can block the main thread
  • adding a persistent store can take a non-trivial amount of time

We solved both issues by adding the persistent store on a background queue. We used a completion handler to notify the owner of the CoreDataManager instance when the Core Data stack is ready to use.

I rarely see developers take the above edge cases into account. Fortunately, the NSPersistentContainer class has you covered. Even though you can access the persistent store coordinator through the persistentStoreCoordinator property, to add a persistent store you need to use the loadPersistentStores(completionHandler:) method.

The method defines one parameter, a completion handler. The completion handler defines two parameters, an NSPersistentStoreDescription instance and an optional error. The completion handler is invoked for every persistent store that is added to the persistent store coordinator.

How does the persistent container know which persistent stores to add? By setting the persistentStoreDescriptions property of the persistent container you can specify which persistent stores to add. This property is of type [NSPersistentStoreDescription], an array of NSPersistentStoreDescription objects.

A NSPersistentStoreDescription object encapsulates the information needed to create a persistent store. It contains information such as the location of the persistent store, the type, and the migration strategy.

If you don't explicitly set the persistentStoreDescriptions property, the persistent container tries to find or create a persistent store based on the name of the persistent container and a set of sensible defaults.

The completion handler of the loadPersistentStores(completionHandler:) method is invoked for each persistent store that is added. That is a bit unfortunate. It means the developer needs to keep track of the state of the Core Data stack. If multiple stores need to be added, you need to make sure you access the Core Data stack when each persistent store is successfully added to the persistent store coordinator.

For a persistent container with one persistent store, the setup is pretty simple. In the example below, I create a persistent container in a view controller and add the persistent store in the viewDidLoad() method. If adding the persistent store is successful, the user interface is shown to the user by invoking the setupView() method, a helper method.

import UIKit
import CoreData

class ViewController: UIViewController {

    // MARK: - Properties

    let persistentContainer = NSPersistentContainer(name: "Done")

    // MARK: - View Live Cycel

    override func viewDidLoad() {
        super.viewDidLoad()

        persistentContainer.loadPersistentStores { (persistentStoreDescription, error) in
            if let error = error {
                print("Unable to Load Persistent Store")
                print("\(error), \(error.localizedDescription)")

            } else {
                DispatchQueue.main.async {
                    self.setupView()
                }
            }
        }
    }

    // MARK: - View Methods

    private func setupView() {

    }

}

What's Next?

It is clear the NSPersistentContainer class is a welcome addition to the Core Data framework. It fulfills the needs of many Core Data applications and it offers a modern, easy-to-use API.

There is one caveat, though. The downside is that many developers new to Core Data won't both learning the ins and outs of the framework. As a result, they will inevitably run into problems at some point. Don't make that mistake.