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 NSPersistentContainer
s 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
.
- Instantiate a
NSPersistentContainer
(or subclass) with a name and aNSManagedObjectModel
. - Create and configure a
NSPersistentStoreDescription
. - 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.