In this series, we explore several techniques and best practices for building a robust, modern Core Data stack. Modern? Yes. Core Data has undergone several important changes in the past few years. In this series, we discuss what a modern Core Data stack looks like.
The tips, tricks, and best practices discussed in this series are proven recipes for building a robust Core Data application. But I didn't come up with these on my own. Over the years, I have learned a lot from what Apple teaches developers at WWDC and I also picked up many insights from several veteran Core Data developers, such as Marcus Zara.
Persistent Containers
I recently wrote about the new Core Data template that ships with Xcode. The template makes use of the brand new NSPersistentContainer
class, which is available in iOS 10+, tvOS 10+, macOS 10.12+, and watchOS 3+.
This series focuses on a Core Data stack without the NSPersistentContainer
class. While I love the addition of the NSPersistentContainer
class, I strongly believe a proper understanding of the anatomy of the Core Data stack is instrumental to work with the Core Data framework. That's what this series is about.
Bring Your Own
I'm sure you know about the Use Core Data checkbox Xcode provides during project setup and you probably know a library or two that helps you with Core Data. But how do you create a Core Data stack from scratch? How do you make sure you can safely access managed objects from different threads? How do you make sure the user interface doesn't freeze when you save changes to disk?
These questions, and many more, get a proper answer in this series. In the first installment of this series, we start by taking a look at some improvements we can make to the default Core Data stack Xcode gives us when we check the Use Core Data checkbox.
Why Bring Your Own
If you're building an application that makes use of Core Data, I urge you to leave the Use Core Data checkbox unchecked when setting up your project. Having Xcode create a Core Data stack for you is fine if you're learning the framework or if you quickly want to try something out. Even though Xcode tries to be helpful by adding a basic Core Data stack in the application delegate, there are several good arguments for setting up your own Core Data stack.
While I appreciate that Apple makes it easy to get started with Core Data, in the end, it doesn't help anyone. There are a number of issues with the Core Data stack Xcode creates for you. The problem is that developers new to Core Data won't spot or bother with these shortcomings. And more experienced developers won't use the Core Data stack Xcode offers them because they know it won't serve them. In the end, it doesn't help anyone.
Not only is Xcode's implementation less than ideal, the application delegate isn't the best place to set up the Core Data stack of your application. I recommend to take a few minutes and create a separate class that manages your application's Core Data stack. It makes everything related to Core Data easier to manage, more reusable, and, last but certainly not least, more testable. There are no compelling downsides to building your own solution.
The template Xcode provides you with has been updated for Swift and the concurrency API that was introduced several years ago. That said, the template still invokes the abort()
function if the persistent store coordinator fails to add a persistent store. You would be surprised by the number of projects I have seen that ship with this implementation despite Apple's warning that this should not be used in production.
Project Setup
In this episode, I want to lay the foundation for building a robust Core Data stack. Open Xcode and create a new project based on the Single View App template in the iOS > Application section. Set Language to Swift and make sure Use Core Data is unchecked.
Before we create the class that manages the Core Data stack, we need to create a data model we can work with. Create a new data model by choosing the Data Model template from the iOS > Core Data section.
Creating the Core Data Manager
It doesn't matter how you name the class that manages the Core Data stack. I usually stick with CoreDataManager. Create a new Swift file and name it CoreDataManager.swift.
Start by adding an import statement for the Core Data framework at the top and create the CoreDataManager
class.
import CoreData
import Foundation
public class CoreDataManager {
}
I'd like to add a little bit of flexibility to the CoreDataManager
class by passing it the name of the data model during initialization. This makes the class more reusable. Declare a constant property, modelName
, of type String
. The designated initializer of the CoreDataManager
class is straightforward. It accept one argument of type String
, the name of the data model.
import CoreData
import Foundation
public class CoreDataManager {
// MARK: - Properties
private let modelName: String
// MARK: - Initialization
public init(modelName: String) {
// Set Model Name
self.modelName = modelName
}
}
Open AppDelegate.swift and create a private, constant property, coreDataManager
, of type CoreDataManager
. We've already gained an advantage by creating a separate class that manages the Core Data stack. We could, for example, create an instance of the CoreDataManager
class in a root view controller instead of the application delegate. That's a good start.
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Properties
var window: UIWindow?
// MARK: -
private let coreDataManager = CoreDataManager(modelName: "DataModel")
// MARK: - Application Life Cycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
}
Setting Up the Core Data Stack
You probably know that every Core Data stack is composed of three building blocks:
- a persistent store coordinator
- a managed object context
- a managed object model
If you'd like to learn more about the Core Data stack, I recommend reading or watching Exploring the Core Data Stack in which I discuss each of these building blocks in more detail.
The majority of Core Data applications interact exclusively with the managed object context. This means that the Core Data manager shouldn't expose the managed object model and the persistent store coordinator to the rest of the application. They can be declared privately.
Managed Object Model
To set up the Core Data stack, we start with the managed object model. We define a private, lazy, variable property, managedObjectModel
, of type NSManagedObjectModel
. Notice that I make ample use of guard
statement and fatal errors. The initialization of the managed object model should never fail. If the initialization fails, then we have bigger problems to worry about.
// MARK: - Core Data Stack
private lazy var managedObjectModel: NSManagedObjectModel = {
// Fetch Model URL
guard let modelURL = Bundle.main.url(forResource: self.modelName, withExtension: "momd") else {
fatalError("Unable to Find Data Model")
}
// Initialize Managed Object Model
guard let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Unable to Load Data Model")
}
return managedObjectModel
}()
Persistent Store Coordinator
Instantiating the persistent store coordinator is a bit more verbose. We use the managed object model to instantiate the persistent store coordinator. In a do-catch
block, we add a persistent store to the persistent store coordinator. In this example, the persistent store is a SQLite database.
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
}()
Notice that we pass a dictionary of options as the last argument of addPersistentStore(ofType:configurationName:at:options:)
to instruct Core Data to perform pending migrations and infer the mapping model for migrations. If adding the persistent store fails, we log any errors to the console. We implement more robust error handling later in this series.
In this example, the persistent store is located in the documents directory. Where you store the persistent store is up to you.
// Helpers
let fileManager = FileManager.default
let storeName = "\(self.modelName).sqlite"
// URL Documents Directory
let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
Managed Object Context
Last but not least, we create the managed object context. While the implementation isn't difficult, there are a few details worth pointing out.
public private(set) lazy var managedObjectContext: NSManagedObjectContext = {
// Initialize Managed Object Context
let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
// Configure Managed Object Context
managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator
return managedObjectContext
}()
While the managedObjectContext
property is marked as public, the setter is private. Only the CoreDataManager
instance should be allowed to set the managedObjectContext
property.
We initialize the managed object context by invoking init(concurrencyType:)
, the designated initializer of the NSManagedObjectContext
class. The first and only argument of this method is the concurrency type of the managed object context. By passing in .mainQueueConcurrencyType
the managed object context is tied to the main queue. This means that managed object context should only be accessed from the main thread. This is a good start, but we change this later in the series to add more flexibility.
Finally, we set the persistentStoreCoordinator
property of the managed object context and return the managed object context we create in the closure. Notice that the managedObjectContext
is of type NSManagedObjectContext
.
Testing the Core Data Stack
Revisit the application delegate and update the print statement in application(_:didFinishLaunchingWithOptions:)
as shown below. Run the application one more time to verify that everything is working.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(coreDataManager.managedObjectContext)
return true
}
What's Next?
We've made a good start by implementing the CoreDataManager
class. In the next episode of this series, we revisit the managed object context.