Building The Perfect Core Data Stack

Give It Time

Building The Perfect Core Data Stack

The Core Data stack we've built in this series has grown quite a bit in complexity. But if you have a good grasp of the framework, then it isn't rocket science. In this episode, we add the last piece of the puzzle. Revisit the project from the previous episode if you'd like to follow along.

Blocking the Main Thread

The implementation of the closure in which we instantiate the persistent store coordinator currently looks like this.

private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    // Initialize Persistent Store Coordinator
    let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)

    // Helpers
    let fileManager = FileManager.default
    let storeName = "\(self.modelName).sqlite"

    // URL Documents Directory
    let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]

    // URL Persistent Store
    let persistentStoreURL = documentsDirectoryURL.appendingPathComponent(storeName)

    do {
        let options = [
            NSMigratePersistentStoresAutomaticallyOption : true,
            NSInferMappingModelAutomaticallyOption : true
        ]

        // Add Persistent Store
        try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
                                                          configurationName: nil,
                                                          at: persistentStoreURL,
                                                          options: options)

    } catch {
        fatalError("Unable to Add Persistent Store")
    }

    return persistentStoreCoordinator
}()

In a do-catch statement, we add the persistent store to the persistent store coordinator. What Apple's documentation doesn't mention is that addPersistentStore(ofType:configurationName:at:options:) doesn't return immediately. It can return in a fraction of a second or it can take longer, much longer. And that can be a problem.

If the persistent store needs to be migrated, for example, adding the persistent store can take longer than you anticipated. And the time it takes differs from user to user. If the user has added thousands of records to the persistent store and uses an older device, it can take longer.

This isn't a problem were it not that we add the persistent store on the main thread. We can resolve this by adding the persistent store on a background thread using grand central dispatch.

Accessing the Core Data Stack

Adding the persistent store on a background thread is only half the solution. If the application attempts to access the Core Data stack before it's initialized, bad things can happen. The behavior is undefined. This means that we need to notify the application when it's safe to access the Core Data stack, that is, when it's initialized and ready to use.

This isn't difficult either. We pass the Core Data manager a closure, which it invokes when the Core Data stack is initialized. The application can then respond appropriately.

Now that we know which changes we need to make, it's time to refactor the CoreDataManager class.

Refactoring the Core Data Manager

Initializing the Core Data Manager

We first need to update the initializer of the CoreDataManager class. We define a second parameter of type () -> (), a closure with no parameters and no return value. Notice that we store the completion handler in a property, completion. We also define a type alias for convenience.

import CoreData
import Foundation

public class CoreDataManager {

    // MARK: - Type Aliases

    public typealias CoreDataManagerCompletion = () -> ()

    // MARK: - Properties

    private let modelName: String

    // MARK: -

    private let completion: CoreDataManagerCompletion

    // MARK: - Initialization

    public init(modelName: String, completion: @escaping CoreDataManagerCompletion) {
        // Set Properties
        self.modelName = modelName
        self.completion = completion

        // Setup Core Data Stack
        setupCoreDataStack()
    }

    ...

}

The @escaping attribute is required since the closure that is passed to the initializer is invoked after the initializer returns. In other words, the closure is escaping. If you want to learn more about escaping closures and the @escaping attribute, I recommend reading a tutorial I recently wrote about this topic.

The setupCoreDataStack() method is a helper method we implement shortly.

Initializing the Persistent Store Coordinator

We also need to refactor the closure in which we initialize the persistent store coordinator. We no longer add the persistent store to the persistent store coordinator when it's initialized. We initialize the persistent store coordinator using the managed object model and return it.

private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    return NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
}()

Setting Up the Core Data Stack

In setupCoreDataStack(), the helper method we invoke in the initializer, we ask the main managed object context for its persistent store coordinator. This ensures the Core Data stack is set up.

Using Grand Central Dispatch, we invoke addPersistentStore(to:), another helper method, on a background queue, passing in the persistent store coordinator. And, more importantly, we asynchronously invoke the completion handler on the main queue. This setup ensures the completion handler is invoked after the persistent store is added to the persistent store coordinator.

private func setupCoreDataStack() {
    // Fetch Persistent Store Coordinator
    guard let persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator else {
        fatalError("Unable to Set Up Core Data Stack")
    }

    DispatchQueue.global().async {
        // Add Persistent Store
        self.addPersistentStore(to: persistentStoreCoordinator)

        // Invoke Completion On Main Queue
        DispatchQueue.main.async { self.completion() }
    }
}

The implementation of addPersistentStore(to:) should look familiar. It contains the code we removed from the closure in which we initialize the persistent store coordinator.

private func addPersistentStore(to persistentStoreCoordinator: NSPersistentStoreCoordinator) {
    // Helpers
    let fileManager = FileManager.default
    let storeName = "\(self.modelName).sqlite"

    // URL Documents Directory
    let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]

    // URL Persistent Store
    let persistentStoreURL = documentsDirectoryURL.appendingPathComponent(storeName)

    do {
        let options = [
            NSMigratePersistentStoresAutomaticallyOption : true,
            NSInferMappingModelAutomaticallyOption : true
        ]

        // Add Persistent Store
        try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
                                                          configurationName: nil,
                                                          at: persistentStoreURL,
                                                          options: options)

    } catch {
        fatalError("Unable to Add Persistent Store")
    }
}

These are the changes we need to make to the CoreDataManager class. We can now focus on the changes we need to make to the application to benefit from the refactoring.

Setting Up the Core Data Stack

View controller containment is a pattern I very often use because it gives me a lot of flexibility. It allows me to delegate many setup tasks and responsibilities to a root view controller, keeping the application delegate as concise as possible.

How does this work? Open AppDelegate.swift and update the implementation of the AppDelegate class as shown below. We're going back to the basics.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    // MARK: - Properties

    var window: UIWindow?

    // MARK: - Application Life Cycle

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }

}

Rename the ViewController class to RootViewController. This view controller is the view controller that acts as the container view controller. It lives as long as the application lives. This means that it's a good place for setting up the Core Data stack, which looks something like this.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    private var coreDataManager: CoreDataManager?

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup Core Data Manager
        coreDataManager = CoreDataManager(modelName: "DataModel", completion: {
            self.setupView()
        })
    }

    // MARK: - View Methods

    private func setupView() {
        // Configure View
        view.backgroundColor = .red
    }

}

We declare a private property, coreDataManager, of type CoreDataManager?. In the viewDidLoad() method, we initialize a CoreDataManager instance and assign it to the coreDataManager property. In the completion handler that is passed to the initializer of the CoreDataManager class, we invoke setupView(). In setupView(), we can start accessing the Core Data stack and set up the user interface.

If you run the application, you'll notice that everything happens very fast. Setting up the Core Data stack takes up quite a bit of resources, but it still happens very, very quickly.

Is This Really Necessary

Chances are that you've been wondering why this is necessary. Is it necessary to add this complexity for something that happens very rarely. The truth is that you don't know if it happens rarely and when it happens.

If you decide to ship a major change of your data model in the next release of your application that requires a complex migration, you may think differently, especially if crash reports start popping up.

As software developers, we try to control as much of the environment as possible. But there are aspects we don't or can't control. We don't control how much data the user stores in the applications we build. And we don't control what type of device the application runs on.

A surprising number of users doesn't make backups and the only data they have is stored locally on the device. If something goes wrong due to a bad migration, they might lose the data they've added to your application for years and years. That's something we need to avoid at any cost. If that means adding a bit more complexity, then that's the solution.

Apple Leads the Way

This episode was in part inspired by the addition of a new method to the NSPersistentStoreCoordinator class, addPersistentStore(with:completionHandler:). This method is available in iOS 10+, tvOS 10+, macOS 10.12+, and watchOS 3+.

As the name implies, the method allows developers to add a persistent store to a persistent store coordinator. The second parameter of this method is a completion handler that is invoked when this operation completes. With this addition, Apple shows us that this is an important aspect of setting up the Core Data stack.

Don't assume that setting up the Core Data stack is always going to take a split second. Prepare for the worst.