It's time to refactor the CoreDataManager class. Let me walk you through the changes we need to make. Don't worry, though, most of the implementation of the CoreDataManager class remains unchanged.

Creating a Private Managed Object Context

We start by creating a privateManagedObjectContext property of type NSManagedObjectContext. It's a private, lazy property.

CoreDataManager.swift

private lazy var privateManagedObjectContext: NSManagedObjectContext = {

}()

We initialize a managed object context by invoking init(concurrencyType:). The concurrency type tells Core Data how the managed object context should be managed from a concurrency perspective. What does that mean? The Core Data framework defines three concurrency types:

  • mainQueueConcurrencyType
  • confinementConcurrencyType
  • privateQueueConcurrencyType

The first concurrency type, mainQueueConcurrencyType, associates the managed object context with the main queue. This is important if the managed object context is used in conjunction with view controllers or is linked to the application's user interface.

By setting the concurrency type to privateQueueConcurrencyType, the managed object context is given a private dispatch queue for performing its operations. The operations performed by the managed object context are not performed on the main thread. That's key.

The confinementConcurrencyType concurrency type used to be the default. If you create a managed object context by invoking init(), the concurrency type is set to confinementConcurrencyType. However, as of iOS 9, the init() method of the NSManagedObjectContext class is deprecated. A managed object context should only be created by invoking init(concurrencyType:), passing mainQueueConcurrencyType or privateQueueConcurrencyType as the argument.

Because we're creating a private managed object context, we pass privateQueueConcurrencyType as the argument of init(concurrencyType:).

CoreDataManager.swift

// Initialize Managed Object Context
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)

We set the persistentStoreCoordinator property of the private managed object context. This means that a save operation pushes any changes of the managed object context to the persistent store coordinator, which pushes the changes to the persistent store.

CoreDataManager.swift

// Configure Managed Object Context
managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator

We return the managed object context from the closure. This is what the implementation of the privateManagedObjectContext property looks like.

CoreDataManager.swift

private lazy var privateManagedObjectContext: NSManagedObjectContext = {
    // Initialize Managed Object Context
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)

    // Configure Managed Object Context
    managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator

    return managedObjectContext
}()

Updating the Main Managed Object Context

The next step is updating the implementation of the managedObjectContext property. First, rename the property to mainManagedObjectContext to show that the managed object context is associated with the application's main dispatch queue.

CoreDataManager.swift

private(set) lazy var mainManagedObjectContext: NSManagedObjectContext = {
    ...
}()

With what we learned in the previous episode still fresh in your mind, the change we need to make is easy. The managed object context is created by invoking init(concurrencyType:), passing in mainQueueConcurrencyType as the argument. Instead of setting the persistentStoreCoordinator property of the managed object context, we set its parent property to the private managed object context we created a few moments ago.

CoreDataManager.swift

private(set) lazy var mainManagedObjectContext: NSManagedObjectContext = {
    // Initialize Managed Object Context
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

    // Configure Managed Object Context
    managedObjectContext.parent = self.privateManagedObjectContext

    return managedObjectContext
}()

This means that a save operation pushes changes from the main managed object context to the private managed object context. From a performance point of view, this is more than sufficient for the vast majority of applications that make use of Core Data.

Updating the Save Method

We're not done yet. We also need to update the saveChanges() method. It changes pretty dramatically. Before we push any changes from the private managed object context to the persistent store, we need to push the changes from the main managed object context to the private managed object context.

Saving those changes needs to happen on the queue of the managed object context. But how do we know what queue that is? And how can we access that queue?

Fortunately, the Core Data framework can help us with that. To make sure a managed object context is accessed on the queue it's associated with, you use the perform(_:) and performAndWait(_:) methods.

Both methods accept a closure and the Core Data framework guarantees that the closure is invoked on the queue the managed object context is associated with. The only difference between both methods is that performAndWait(_:) is performed synchronously. As the name implies, it blocks the thread it's called on.

With this in mind, we can continue implementing the saveChanges() method. We invoke performAndWait(_:) on the main managed object context and, in the closure, we save the main managed object context if it has any changes.

CoreDataManager.swift

private func saveChanges() {
    mainManagedObjectContext.performAndWait {
        do {
            if self.mainManagedObjectContext.hasChanges {
                try self.mainManagedObjectContext.save()
            }
        } catch {
            let saveError = error as NSError
            print("Unable to Save Changes of Main Managed Object Context")
            print("\(error), \(error.localizedDescription)")
        }
    }
}

Next, we invoke perform(_:) on the private managed object context and in the closure we save the changes of the private managed object context if it has any. This means the private managed object context pushes its changes to the persistent store coordinator.

CoreDataManager.swift

private func saveChanges() {
    mainManagedObjectContext.performAndWait {
        do {
            if self.mainManagedObjectContext.hasChanges {
                try self.mainManagedObjectContext.save()
            }
        } catch {
            let saveError = error as NSError
            print("Unable to Save Changes of Main Managed Object Context")
            print("\(error), \(error.localizedDescription)")
        }
    }

    self.privateManagedObjectContext.perform {
        do {
            if self.privateManagedObjectContext.hasChanges {
                try self.privateManagedObjectContext.save()
            }
        } catch {
            print("Unable to Save Changes of Private Managed Object Context")
            print("\(error), \(error.localizedDescription)")
        }
    }
}

Notice that we first save the changes of the main managed object context. This is important because we need to make sure the private managed object context includes the changes of its child managed object context.

For this reason, we use performAndWait(_:) instead of perform(_:). We first want to make sure the changes of the main managed object context are pushed to the private managed object context before pushing the changes of the private managed object context to the persistent store coordinator.

Another Option

There is another option. Take a look at this implementation of the saveChanges() method.

CoreDataManager.swift

private func saveChanges() {
    mainManagedObjectContext.perform {
        do {
            if self.mainManagedObjectContext.hasChanges {
                try self.mainManagedObjectContext.save()
            }
        } catch {
            print("Unable to Save Changes of Main Managed Object Context")
            print("\(error), \(error.localizedDescription)")
        }

        self.privateManagedObjectContext.perform {
            do {
                if self.privateManagedObjectContext.hasChanges {
                    try self.privateManagedObjectContext.save()
                }
            } catch {
                print("Unable to Save Changes of Private Managed Object Context")
                print("\(error), \(error.localizedDescription)")
            }
        }
    }
}

The result is similar. We push the changes of the main managed object context to the private managed object context by invoking save() on the main managed object context. But notice that we use perform() instead of performAndWait(_:).

After pushing the changes of the main managed object context to its parent, we save the changes of the private managed object context. We do this within the closure we pass to the perform(_:) method of the main managed object context. This means that the changes of the main managed object context are saved before those of the private managed object context are pushed to the persistent store coordinator.

When to Save

We currently save the changes of the managed object contexts when the application is about to be terminated and when it's pushed to the background. This is fine, but you need to keep in mind that a crash of your application results in data loss if you adopt this strategy. Any changes that aren't pushed to the persistent store coordinator when the application is suddenly terminated are lost.

You could adopt an alternative approach by saving changes in the private managed object context at regular time intervals or when you know the user isn't actively using your application. This depends on the type of application you're creating. There is no one solution. That's important to know and understand.

Your knowledge of the Core Data framework has grown quite a bit. Even though concurrency may seem to be an advanced topic, it isn't. If you work with Core Data, then you need to know how Core Data behaves in a multithreaded environment.