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
See books

I 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

Filip Němeček profile photo

WRITTEN BY

Filip Němeček @nemecek_f@iosdev.space

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀