Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to deal with persistentContainer instance on AppDelegate

When we create a new project in Xcode with the Core Data option checked, it generates a new project defining Core Data stack on AppDelegate.swift:

class AppDelegate: UIResponder, UIApplicationDelegate {

    // ...

    // MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "CoreDataTest")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

}

For me to easily access persistentContainer, I've also added this piece of code:

static var persistentContainer: NSPersistentContainer {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { fatalError("Could not convert delegate to AppDelegate") }
    return appDelegate.persistentContainer
}

So I can call it like this:

let container = AppDelegate.persistentContainer

The problem happens when I try to access it from a background thread. For example, I have a piece of code that runs in the background and fetches some data from a web service. When it gets the data, I'm saving it using:

static func loadData(_ id: String) {
    fetchDataOnBackground(id: id) { (error, response) in
        if let error = error {
            // handle...
            return
        }

        guard let data = response?.data else { return }

        let container = AppDelegate.persistentContainer // Here
        container.performBackgroundTask({ context in
            // save data...
        })
    }
}

When I try to get the persistent container, it generates on the console:

Main Thread Checker: UI API called on a background thread: -[UIApplication delegate]

For this to not happen anymore, I've changed my persistentContainer from lazy var to static on AppDelegate:

static var persistentContainer: NSPersistentContainer = {
    // same code as before...
}()

And the error doesn't happen anymore.

But I'm wondering if this can have any side effects that I'm not aware of. I mean, I would have only one persistentContainer anyway, because there's only one instance of AppDelegate right? So I can change it to static like I did and access it using AppDelegate.persistentContainer on other parts of my app without any problems?

Or is there another recommended pattern to handle persistentContainer instantiation and usage?

like image 814
Marcos Tanaka Avatar asked Jan 27 '23 11:01

Marcos Tanaka


1 Answers

Introduction

Hello. I myself am working with core data for an app and I have found that there are a few ways to approach core data handling and with threading it adds another layer.

As Apple states in their documentation, you should not pass NSManagedObjectContext between threads, due to their inner functionality. And by default all UI updates should be done on the main thread. So I would suggest that you do the saving on the main thread, after you've fetched the data using your background thread method. As a general rule I would try to follow this, but I don't know if your project necessitates the background save?

Issue etiology

The problem arrives from that you instantiate the container in the background thread. However, when it is declared as static in the app delegate, only one initialization occurs and it is not initialized on the background thread which disturbs its use.

From apple API for NSManagedObjectContext Apple API website:

Core Data uses thread (or serialized queue) confinement to protect managed objects and managed object contexts (see Core Data Programming Guide). A consequence of this is that a context assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method. You should not, therefore, initialize a context on one thread then pass it to a different thread. Instead, you should pass a reference to a persistent store coordinator and have the receiving thread/queue create a new context derived from that. If you use Operation, you must create the context in main (for a serial queue) or start (for a concurrent queue).

Approaches to initialize core data stack

  1. Don't initialize and setup your core data stack in the app delegate. Use a NSObject subclass and keep that as core data stack (Code from ray wenderlich tutorial. Ray Wenderlich tutorial (1 year old)). If used you should initialize this in app delegate then pass it around. But remember your issue stems from threading so you need to use a static var as you did or a more recommended way, save to core data after the fetch has completed and you've exited the background thread.:

    class CoreDataStack: NSObject {
        let moduleName = "YourModuleName"
    
        func saveToMainContext() { // Just a helper method for removing boilerplate code when you want to save. Remember this will be done on the main thread if called.
            if objectContext.hasChanges {
                do {
                    try objectContext.save()
                } catch {
                    print("Error saving main ManagedObjectContext: \(error)")
                }
            }
        }
    
        lazy var managedObjectModel: NSManagedObjectModel = {
            let modelURL = Bundle.main.url(forResource: moduleName, withExtension: "momd")!
            return NSManagedObjectModel(contentsOf: modelURL)!
        }()
    
        lazy var applicationDocumentsDirectory: URL = {
            return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
        }()
    
        lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
            let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
    
            let persistenStoreURL = self.applicationDocumentsDirectory.appendingPathComponent("\(moduleName).sqlite")
    
            do {
                try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: persistenStoreURL, options: [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption : true])
            } catch {
                fatalError("Persistent Store error: \(error)")
            }
            return coordinator
        }()
    
        lazy var objectContext: NSManagedObjectContext = {
            let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) // As stated in the documentation change this depending on your need, but i recommend sticking to main thread if possible.
    
            context.persistentStoreCoordinator = self.persistentStoreCoordinator
            return context
        }()
    }
    
  2. Using the app delegate as setup. I usually initialize objects from the app delegate with (UIApplication.shared.delegate as! AppDelegate).persistentContainer when they aren't static and I have to initialize then from there, which would reference the current used Application delegate. However that might not matter.

    • Alternatively you can use static instead in the app delegate.

Hopefully I'm not to late out for this. Perhaps it helps someone else otherwise. Good luck.

like image 197
theoadahl Avatar answered Feb 13 '23 07:02

theoadahl