Previously, we updated the Core Data stack by adding a private managed object context. Remember that the private managed object context performs two tasks:
- it pushes its changes to the persistent store coordinator
- it acts as the parent of the main managed object context
This is what the Core Data stack currently looks like.
It's important to understand that the private managed object context should only be accessed by the CoreDataManager
class. Other objects, such as view controllers, use the main managed object context to interact with the Core Data stack.
But this can result in threading and performance issues if the main managed object context is used by objects that shouldn't be using it. For example, what happens if an object performs a long-running operation using the main managed object context to fetch data from the persistent store? What happens if the application downloads data in the background and inserts it into the persistent store?
In these scenarios, it's better to create a separate managed object context that operates on a background thread. The Core Data stack we're after looks something like this.
More Child Managed Object Contexts
As I mentioned earlier in this series, creating a managed object context is a relatively cheap operation. It's often better to create a separate, private managed object context than to use the same managed object context throughout your application. This is especially true for tasks that run in the background or tasks that take a non-trivial amount of time to complete.
The good news is that it's easy to accomplish and implement such a solution. In this episode, I want to show you how easy it is to add the ability to the CoreDataManager
class to create a new managed object context that is a child of the main managed object context. You can use such a private managed object context for tasks that shouldn't be executed on the main thread.
When to Create a Child Managed Object Context
Even though creating a managed object context is a cheap operation, you need to understand when it's appropriate to create a child managed object context. A very common example involves background operations using an Operation
subclass.
The Operation
class is a very useful class of the Foundation framework. It performs a specific task on a background queue. Together with OperationQueue
, operations are incredibly useful, flexible, and powerful.
In Samsara, I use a collection of Operation
subclasses to query the persistent store for the user's statistics. Every Operation
class has its own managed object context, which makes it very easy to encapsulate a specific task in the operation.
The child managed object context of an Operation
subclass should do its work on a private queue. Creating and using a managed object context in an Operation
subclass that performs its work on the main queue defeats the purpose of having a separate managed object context.
This brings up an interesting question. Should you ever have multiple managed object contexts that perform work on the main queue? That's a perfectly valid question. There's no right or wrong answer to this question. For example, I've seen examples in which a child managed object context linked to the main thread is used for adding or editing records. If the user decides not to save their changes, then the managed object context is discarded along with any changes it keeps track of. This eliminates the necessity to manually delete or revert the managed object.
This certainly isn't an issue in terms of performance, but it's important to asses the complexity that is added compared to what is gained. I can see the benefits if a complex record or group of records is created or modified.
Updating the Core Data Manager
Adding the ability to instantiate a private managed object context is straightforward thanks to the groundwork we laid in the previous episodes. This is what the implementation looks like.
public func privateChildManagedObjectContext() -> NSManagedObjectContext {
// Initialize Managed Object Context
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// Configure Managed Object Context
managedObjectContext.parent = mainManagedObjectContext
return managedObjectContext
}
The implementation is simple. You can further configure the managed object context by setting its merge policy, giving it a name to facilitate debugging, and setting a staleness interval. That's up to you and it depends on the needs of your application.
How would we use a managed object context created with the privateChildManagedObjectContext()
method? This is best illustrated by creating an Operation
subclass. The subclass I have in mind looks something like this.
import CoreData
import Foundation
class Task: Operation {
// MARK: - Properties
private let managedObjectContext: NSManagedObjectContext
// MARK: - Initialization
init(managedObjectContext: NSManagedObjectContext) {
// Set Managed Object Context
self.managedObjectContext = managedObjectContext
super.init()
}
// MARK: - Overrides
override func main() {
// ...
// Save Changes
saveChanges()
}
// MARK: - Helper Methods
private func saveChanges() {
managedObjectContext.performAndWait {
do {
if self.managedObjectContext.hasChanges {
try self.managedObjectContext.save()
}
} catch {
print("Unable to Save Changes of Managed Object Context")
print("\(error), \(error.localizedDescription)")
}
}
}
}
The designated initializer accepts the private managed object context as a parameter. Before exiting the main()
method of the Operation
subclass, the changes of the managed object context are saved. This means that the changes made in the private managed object context are pushed to its parent managed object context, the main managed object context of the CoreDataManager
class.
Notice that we perform the save operation by invoking performAndWait(_:)
to ensure the operation isn't deallocated before the save operation completes.
How would we use the Task
class? In the next example, we instantiate an Task
instance and add it to an operation queue.
// Create Private Childe Managed Object Context
let managedObjectContext = coreDataManager.privateChildManagedObjectContext()
// Initialize Operation
let task = Task(managedObjectContext: managedObjectContext)
// Add Operation to Operation Queue
operationQueue.addOperation(task)
What's Next?
Even though the CoreDataManager
class gained a little in complexity, we now have the ability to create additional, private managed object contexts when needed.
The next episode focuses on another important aspect of working with Core Data in a Cocoa project, accessing the Core Data stack. In that episode, we discuss singletons and dependency injection.