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?
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?
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).
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
}()
}
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.
static
instead in the app delegate. Hopefully I'm not to late out for this. Perhaps it helps someone else otherwise. Good luck.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With