My approach to setting up Core Data stack
This post details how I set up Core Data in my projects and also explains what and why. It includes working with background context.
Published: Aug. 5, 2020 Sponsored See booksI got asked on the Hacking With Swift forums about my approach to configuring Core Data outside AppDelegate
and decided to share my solution here. I personally think Core Data code has no business to be inside AppDelegate
and it would be nice if Xcode changed this default.
Anyway let's look at the code. However a small notice. I don't want to claim that this is THE ONLY WAY of using Core Data and it may not be optimal in some use cases. On the other hand I am using this code in a few projects and it works great, including the background stuff. Plus I have read the excellent Core Data book by objc.io (most chapters multiple times).
Complete code
Let's look on the whole class which I call Database
first and then we can discuss the code.
import Foundation
import CoreData
class Database {
static let shared = Database()
private var persistentContainer: NSPersistentContainer!
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
private init() {
NotificationCenter.default.addObserver(self, selector: #selector(backgroundContextDidSave(notification:)), name: .NSManagedObjectContextDidSave, object: nil)
}
@objc func backgroundContextDidSave(notification: Notification) {
guard let notificationContext = notification.object as? NSManagedObjectContext else { return }
guard notificationContext !== context else {
return
}
context.perform {
self.context.mergeChanges(fromContextDidSave: notification)
}
}
func performBackgroundTask(block: @escaping (NSManagedObjectContext) -> Void) {
persistentContainer.performBackgroundTask(block)
}
func prepare() {
persistentContainer = NSPersistentContainer(name: "Model")
persistentContainer.loadPersistentStores { storeDescription, error in
if let error = error {
print("Unresolved error \(error)")
fatalError()
}
}
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true
}
func saveContext() {
do {
if persistentContainer.viewContext.hasChanges {
try persistentContainer.viewContext.save()
}
} catch {
print(error.localizedDescription)
}
}
}
As you can see I am using it as a singleton which also means the shared
instance it lazily created. There is one "drawback" and that is you need to manually call Database.shared.prepare()
when your app launches. Ideally in the AppDelegate
.
I had some issues with the lazy property approach and decided to stick with this. You also have control over the initialization and could easily pass the .xcdatamodel
name as a parameter for greater flexibility.
Performing background work
I am also setting the merging properties which are needed for work with background context. Since the main context should always operate on the main thread, having convenience method performBackgroundTask
allows me to perform heavy background work without affecting the UI. For example I am using this to do iCloud Drive upload and OCR in my app Scan it.
The method usage looks like this:
Database.shared.performBackgroundTask { (context) in
// work with context as you normally would with the main one
// when changing properties of Core Data objects dont forget to call context.save()
}
Warning: Unfortunately there is an issue when you execute multiple background tasks at the same time on the same data. Core Data will report error with merging changes. Once I figure out how to fix this, I will update the post.
Merging changes
Since main context has no idea that database has been changed from the background, there is observer for the NSManagedObjectContextDidSave
notification created in the init
.
And the actual merging:
@objc func backgroundContextDidSave(notification: Notification) {
guard let notificationContext = notification.object as? NSManagedObjectContext else { return }
guard notificationContext !== context else {
return
}
context.perform {
self.context.mergeChanges(fromContextDidSave: notification)
}
}
First we get the context that sent the notification, then confirm it is not the main one and finally use perform
method to dispatch on the context's queue before merging changes from the notification.
Injecting this into SwiftUI
So far I don't have a lot of SwiftUI experience so I am going just mention how to add this solution into the environment.
Assuming you did not check "Use Core Data" for your project, you can open SceneDelegate
and find this line:
let contentView = ContentView()
Modify this to:
let contentView = ContentView()
.environment(\.managedObjectContext, Database.shared.context)
And done. The managed object context is available. Just like when you check "Use Core Data".
If you find any problems with this approach, please let me know over at Twitter.
Uses: Xcode 11 & Swift 5