In the previous tutorial, we discussed Core Data in the light of concurrency. I hope that lesson has taught you that Core Data and concurrency can go hand in hand. As long as you remember the basic rules we discussed, you should be fine.
There are a few APIs we didn't discuss in detail in the previous tutorial, perform(_:)
and performAndWait(_:)
. In this tutorial, I also talk about working with child managed object contexts when performing isolated Core Data operations, such as downloading and processing data in the background.
Performing Operations
You already know that you should invoke init(concurrencyType:)
to create a managed object context, no matter how or where you plan to use the managed object context. You also learned that a managed object context should only be accessed from the thread it's associated with. But how do you know which thread a managed object context is associated with? And how do you make sure the managed object context is only accessed from that thread? It's time to answer these questions.
But before I address these questions, you need to understand what init(concurrencyType:)
does behind the scenes. If you invoke init(concurrencyType:)
and pass in privateQueueConcurrencyType
as the concurrency type, Core Data instantiates the managed object context with a private queue. In other words, the managed object context is created on a background thread even though you may have instantiated it on the main thread. This is important to understand.
But how do we make sure we access a managed object context on the thread it is associated with? Two methods make this easy and transparent.
perform(_:)
performAndWait(_:)
These methods guarantee that the closure you hand it is executed on the queue the managed object context is associated with. Take a look at the example below to see how this works in practice.
managedObjectContext.perform {
// Create Note
let note = Note(context: managedObjectContext)
// Populate Note
note.content = ""
note.title = title
note.updatedAt = NSDate()
note.createdAt = NSDate()
do {
try note.managedObjectContext?.save()
} catch {
let saveError = error as NSError
print("Unable to Save Note")
print("\(saveError), \(saveError.localizedDescription)")
}
}
If you're still confused, then remember that every managed object context that is initialized by invoking init(concurrencyType:)
is associated with a dispatch queue, a private queue (privateQueueConcurrencyType
) or the main queue (mainQueueConcurrencyType
).
The perform(_:)
and performAndWait(_:)
methods schedule the closure that you pass them on the queue of the managed object context. This ensures the operation you perform on the managed object context occurs on the correct thread.
The only difference between perform(_:)
and performAndWait(_:)
is how the queue handles the closure. The perform(_:)
method schedules the closure and immediately returns control. It doesn't wait for the closure to finish execution.
As the name implies, performAndWait(_:)
schedules the closures and returns control when the closure has finished executing. In other words, performAndWait(_:)
executes the closure synchronously whereas perform(_:)
executes the closure asynchronously.
With this in mind, the implementation of the saveChanges(_:)
method of the CoreDataManager
class should make more sense. We call perform(_:)
on the main managed object context to make sure its changes are pushed to the private managed object context before the changes of the private managed object context are pushed to the persistent store coordinator. Notice that we save the changes of the private managed object context in the closure we pass to the perform(_:)
call of the main managed object context.
// MARK: - Notification Handling
@objc func saveChanges(_ notification: NSNotification) {
managedObjectContext.perform {
do {
if self.managedObjectContext.hasChanges {
try self.managedObjectContext.save()
}
} catch {
let saveError = error as NSError
print("Unable to Save Changes of Managed Object Context")
print("\(saveError), \(saveError.localizedDescription)")
}
self.privateManagedObjectContext.perform {
do {
if self.privateManagedObjectContext.hasChanges {
try self.privateManagedObjectContext.save()
}
} catch {
let saveError = error as NSError
print("Unable to Save Changes of Private Managed Object Context")
print("\(saveError), \(saveError.localizedDescription)")
}
}
}
}
By invoking perform(_:)
on both managed object contexts, the closures in which we save the changes of the managed object contexts are executed asynchronously. There is no need to use performAndWait(_:)
in this scenario.
Child Managed Object Contexts
The Operation
class is often used for executing tasks in the background. Core Data can work beautifully with Operation
. Now that we know how to use Core Data on multiple threads, using Core Data in combination with Operation
is no longer a daunting task.
What we need to do is:
- create a private managed object context for the operation to use
- set the parent managed object context of the private managed object context
- push the changes of the child managed object context when the operation completes
As long as we use perform(_:)
and performAndWait(_:)
to perform operations on the managed object context, we won't run into multithreading issues. Remember that managed object contexts are cheap to create and creating one specifically for an operation is fine. In fact, it is the correct thing to do.
By creating a private managed object context, you don't need to worry about blocking the main managed object context, no matter what happens in the operation. Let me show you how this works in practice. This is what the interface of the Operation
subclass looks like.
import CoreData
import Foundation
class CoreDataOperation: Operation {
// MARK: - Properties
private let privateManagedObjectContext: NSManagedObjectContext
// MARK: - Initialization
init(parentManagedObjectContext: NSManagedObjectContext) {
// Initialize Private Managed Object Context
self.privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// Configure Private Managed Object Context
self.privateManagedObjectContext.parent = parentManagedObjectContext
}
}
We create a private, constant property, privateManagedObjectContext
, of type NSManagedObjectContext
. This property is initialized in the designated initializer of the Operation
subclass, init(parentManagedObjectContext:)
. There are three important elements to the interface:
The concurrency type of the private managed object context is
privateManagedObjectContext
. This ensures any operations we perform using the private managed object context won't interfere with what happens on the main thread.We set the parent managed object context of the private managed object context to the managed object context that is passed to the initializer. Remember that this means that the changes of the private managed object context are pushed to its parent managed object context when a save operation is performed.
Because we declare a private, constant property that isn't an optional, the designated initializer of the
Operation
class is no longer a designated initializer. This is important and avoids that aCoreDataOperation
instance can be created without providing a parent managed object context.
The operation does its work in the main()
method. At the end of this method, we save the changes of the private managed object context, pushing its changes to the parent managed object context.
// MARK: - Overrides
override func main() {
...
// Save Changes
saveChanges()
}
The implementation of saveChanges()
isn't difficult to understand.
// MARK: - Helper Methods
private func saveChanges() {
privateManagedObjectContext.perform {
guard self.privateManagedObjectContext.hasChanges else { return }
do {
try self.privateManagedObjectContext.save()
} catch {
let saveError = error as NSError
print("Unable to Save Changes")
print("\(saveError), \(saveError.localizedDescription)")
}
}
}
Now that you know what Core Data is and how the Core Data stack is set up, it's time to write some code. If you're serious about Core Data, check out Mastering Core Data With Swift. We build an application that is powered by Core Data and you learn everything you need to know to use Core Data in your own projects.