A Technique For Creating a NSPersistentContainer

August 24, 2021

There are numerous ways to create and configure a NSPersistentContainer based on your app's needs. A lot of apps fall into the category of a persistent container configured with a single NSManagedObjectModel backed by a SQLite NSPersistentStore. This article looks at one technique for standing up this type of Core Data stack that is more or less a one-liner at the call site.

This technique works well for enterprise apps that use separate NSPersistentContainers for independent business features, or as a separate package used across different apps.

As we will see, there is not a lot code here, but the solution is interesting.

extension NSPersistentContainer {

    static func makePersistentContainer<P: NSPersistentContainer>(
        named name: String,
        for model: NSManagedObjectModel,
        configurator: PersistentStoreDescriptionConfigurator
    ) throws -> P {
        let persistentContainer = P(name: name, managedObjectModel: model)

        let description = NSPersistentStoreDescription()
        try configurator.configure(description)
        
        persistentContainer.persistentStoreDescriptions = [
            description
        ]

        return persistentContainer
    }
}

Walking Through The Details

In general, there are three main steps to create a NSPersistentContainer.

  1. Instantiate a NSPersistentContainer (or subclass) with a name and a NSManagedObjectModel.
  2. Create and configure a NSPersistentStoreDescription.
  3. Set the NSPersistentStoreDescription on the persistent container.

A future article will discuss techniques for asynchronously loading the persistent container.

For starters, we create an extension on NSPersistentContainer with a generic function that instantiates a NSPersistentContainer of type P. The function uses an app-specific protocol named PersistentStoreDescriptionConfigurator, which we will look at soon, to configure a single persistent store for a given NSPersistentStoreDescription.

Next, the function creates a single NSPersistentStoreDescription and passes it to the PersistentStoreDescriptionConfigurator. The fully configured persistent store description is then registered with the persistent container. Finally, the non-loaded persistent container is returned to the caller ready to be loaded.

Now we can instantiate a persistent container of type P, where P is a NSPersistentContainer, or any persistent container subclass such as NSPersistentCloudKitContainer or an app-specific subclass (e.g. MyAppPersistentContainer).

let name: String = ... 
let model: NSManagedObjectModel = ... 
let configurator: PersistentStoreDescriptionConfigurator = ... 

let container: NSPersistentContainer = try .makePersistentContainer(
    named: name, 
    for: model, 
    configurator: configurator
)

let container: NSPersistentCloudKitContainer = try .makePersistentContainer(
    named: name, 
    for: model, 
    configurator: configurator
)

let container: MyAppPersistentContainer = try .makePersistentContainer(
    named: name, 
    for: model, 
    configurator: configurator
)

Configuring The NSPersistentStoreDescription

The PersistentStoreDescriptionConfigurator protocol is a strategy used by the NSPersistentContainer extension function to configure a NSPersistentStoreDescription just before the store is loaded.

protocol PersistentStoreDescriptionConfigurator {

    func configure(_ description: NSPersistentStoreDescription) throws
}

The configure(_:) method is allowed to throw an error. For example, A SQLite implementation, as seen below, needs to try to create the necessary directories that house the sqlite database files.

struct SQLitePersistentStoreDescriptionConfigurator: PersistentStoreDescriptionConfigurator {

    private let fileURL: URL

    init(directory: URL, name: String) {
        let fileURL = directory
            .appendingPathComponent(name, isDirectory: true)
            .appendingPathComponent(name) // this is the file name
            .appendingPathExtension("sqlite")
        self.init(fileURL: fileURL)
    }

    init(fileURL: URL) {
        self.fileURL = fileURL
    }

    func configure(_ description: NSPersistentStoreDescription) throws {
        description.type = NSSQLiteStoreType

        description.shouldInferMappingModelAutomatically = true
        description.shouldMigrateStoreAutomatically = true
        description.shouldAddStoreAsynchronously = false

        // Set other options here that are relevant to your app.
        // Here are a few examples.
        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        description.setOption(true as NSNumber, forKey: NSSQLiteAnalyzeOption)

        // Create the intermediate directories, if missing. 
        let directory = fileURL.deletingLastPathComponent()
        try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
        description.url = fileURL
    }
}

What's great about the PersistentStoreDescriptionConfigurator strategy is that the configuration of the persistent description is now encapsulated in a concrete type that can be shared, unit tested, and even partitioned using the composite pattern if additional configuration per app or business domain is needed.

Creating A Persistent Container From A Bundle

Often times developers just want to pass a bundle containing the managed object model. Here's a convenience method enabling this.

extension NSPersistentContainer {
    static func makePersistentContainer<P: NSPersistentContainer>(
        forModelsInBundle bundle: Bundle,
        configurator: PersistentStoreDescriptionConfigurator
    ) throws -> P {
        return try makePersistentContainer(
            named: bundle.name,
            for: try mergedModel(from: bundle),
            configurator: configurator
        )
    }

    static func mergedModel(from bundle: Bundle) throws -> NSManagedObjectModel {
        guard
            let model = NSManagedObjectModel.mergedModel(from: [bundle]),
            model.entities.count > 0
        else {
            throw MissingModelInBundleError(bundle: bundle)
        }

        return model
    }
}

The persistent container's name is derived from the bundle's name. Your app probably contains an extension on NSBundle that returns the bundle's name. Here's a solution if you need one.

extension Bundle {

    var name: String {
        return stringValue(for: kCFBundleNameKey)
    }

    private func stringValue(for key: CFString) -> String {
        object(forInfoDictionaryKey: key as String) as! String
    }
}
struct MissingModelInBundleError: LocalizedError {

    let bundle: Bundle

    var errorDescription: String? {
        let template = NSLocalizedString(
            "app-error.missing-model-in-bundle-%@", 
            tableName: nil, 
            bundle: .module, 
            value: "", 
            comment: ""
        )
        return String(format: template, arguments: [bundle.name])
    }
}
let bundle: Bundle = ...  
let configurator: PersistentStoreDescriptionConfigurator = ... 

let container: NSPersistentContainer = try .makePersistentContainer(
    forModelsInBundle: bundle,
    configurator: configurator
)

What's Next?

The next article looks at how to lean on this code to set up unit tests where each test method executes against a completely isolated NSPersistentContainer backed by a SQLite persistent store.