Building The Perfect Core Data Stack

Keeping It Private

Building The Perfect Core Data Stack

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.

A Core Data Stack With Two Managed Object Contexts

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.

A Core Data Stack With Multiple Managed Object Contexts

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.

Next Episode "Passing It Around"