I have an app which takes about a minute to set up, so when the user taps ‘Start New Game’ I want to show an activity spinner while data is loaded into Core Data.
I understand I have to do this on a background thread so I can update the UI on the main thread, but I don’t know how to save the managed context in the background thread. Here is what I have so far:
func startNewGame() {
initiateProgressIndicator() // start the spinner and 'please wait' message
DispatchQueue.global(qos: .background).async {
self.coreDataStack.importDefaultData() // set up the database for the new game
DispatchQueue.main.async {
stopProgressIndicator()
// Transition to the next screen
let vc: IntroViewController = self.storyboard?.instantiateViewController(withIdentifier: "IntroScreen") as! IntroViewController
vc.rootVCReference = self
vc.coreDataStack = self.coreDataStack
self.present(vc, animated:true, completion:nil)
}
}
In importDefaultData() I need to save multiple times but it crashes when I try to do so. I understand now it's because I'm trying to access the main context from a background thread. Here is the basic structure of the function:
func importDefaultData() {
// import data into Core Data here, code not shown for brevity
saveContext()
// import more data into Core Data here, code not shown for brevity
saveContext()
// import final data here
saveContext()
}
From what I have read I think I need to create another managed object context for the background thread, but I am unclear on how to go about this and how to fit it into my current code. I also do not know how to make sure the data is saved correctly before I move on to the next screen. I have no experience with multithreading so any help would be appreciated.
Edit: regarding being marked as duplicate, the suggested topic is 8 years old and doesn't contain any answers. I was hoping for a more up to date answer with perhaps a brief code example in Swift.
Core Data is designed to work in a multithreaded environment. However, not every object under the Core Data framework is thread safe. To use Core Data in a multithreaded environment, ensure that: Managed object contexts are bound to the thread (queue) that they are associated with upon initialization.
Most apps need just a single managed object context. The default configuration in most Core Data apps is a single managed object context associated with the main queue. Multiple managed object contexts make your apps harder to debug; it's not something you'd use in every app, in every situation.
A managed object context represents a single object space, or scratch pad, in a Core Data application. A managed object context is an instance of NSManagedObjectContext . Its primary responsibility is to manage a collection of managed objects.
To save an object with Core Data, you can simply create a new instance of the NSManagedObject subclass and save the managed context. In the code above, we've created a new Person instance and saved it locally using Core Data.
I have solved this after reading the helpful comments from @user1046037 (thanks!) and researching how to use context.perform { }
and context.performAndWait { }
. I will post my code below in case it benefits anyone else, as I couldn't find any examples on SO in swift:
initiateProgressIndicator() // start the spinner and 'please wait' message
let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
childContext.parent = coreDataStack.managedContext
// Create a background task
childContext.perform {
// Perform tasks in a background queue
self.coreDataStack.importDefaultData(childContext: childContext) // set up the database for the new game
do {
// Saves the tasks done in the background to the child context
try childContext.save()
// Performs a task in the main queue and wait until this tasks finishes
self.coreDataStack.managedContext.performAndWait {
do {
// Saves the data from the child to the main context
try self.coreDataStack.managedContext.save()
self.activitySpinner.stopAnimating()
// Transition to the next screen
let vc: IntroViewController = self.storyboard?.instantiateViewController(withIdentifier: "IntroScreen") as! IntroViewController
vc.rootVCReference = self
vc.coreDataStack = self.coreDataStack
self.present(vc, animated:true, completion:nil)
} catch {
fatalError("Failure to save context: \(error)")
}
}
} catch {
fatalError("Failure to save context: \(error)")
}
}
In my CoreDataStack class:
func importDefaultData(childContext: NSManagedObjectContext) {
// import data into Core Data here, code not shown for brevity
saveChildContext(childContext: childContext)
// import more data into Core Data here, code not shown for brevity
saveChildContext(childContext: childContext)
// import final data here
saveChildContext(childContext: childContext)
}
func saveChildContext(childContext: NSManagedObjectContext) {
guard childContext.hasChanges else {
return
}
do {
try childContext.save()
} catch {
let nserror = error as NSError
}
}
If this approach is not best practise or anyone can see a way to improve it, I'd appreciate your thoughts. I should add that I found the following link very helpful: https://marcosantadev.com/coredata_crud_concurrency_swift_1
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