Managed object contexts hierarchy

Denis MukhaDenis Mukha
3 min read

In CoreData we can create multiple managed object contexts using 2 different approaches: connecting all of them directly to the persistent store (left side of the picture) and making a parent-child relationship (right side of the picture)

Let's compare these 2 approaches.

Firstly, we need to understand how data is synchronised between contexts in both approaches. In case when contexts are connected directly to the persistent store coordinator they need to sync data through the persistent store. So basically when one context saves any data to the store another one should re-fetch them. To do so and merge changes from one context into another CoreData provides NSManagedObjectContextDidSave notification. Let's see how this synchronisation works. For example, we have 2 contexts: main and background. We do all the work for data updating on background context and want to have an up-to-date state on the main context. In that case, we need to subscribe on NSManagedObjectContextDidSave notification from the background context. This notification will be sent each time the func save() throws is called on the context. When notification is delivered we need to dispatch to the thread of the main context and call func mergeChanges(fromContextDidSave notification: Notification) on the main context providing received notification. Dispatching to the context thread is essential because operations with ManagedObjects are not thread-safe (only objectID can be passed between the threads but about that later). Under the hood when the merge changes function is performed only objects that are already in the main context get updates from the persistent store coordinator applying the merge policy that is specified for the main context. Let's see what this logic looks like in the code:

let mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
mainContext.persistentStoreCoordinator = coordinator
mainContext.mergePolicy = NSMergePolicy.rollback
mainContext.undoManager = nil

let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.persistentStoreCoordinator = coordinator
backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
backgroundContext.undoManager = nil

NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextDidSave,
                                       object: backgroundContext,
                                       queue: nil) { [weak mainContext] (notification) in 
    guard let mainContext else { return }
    mainContext.perform { [weak mainContext] in
        guard let mainContext else { return }
        mainContext.perform.mergeChanges(fromContextDidSave: notification)
    }
}

So now we guarantee that all the changes saved in the background context are merged into the main context. Also, the notification posted by CoreData contains all the information about all the changes in ManagedObjects that were saved in the background context - Later we will dive deeper into this notification and how it can be used to observe data changes in the app.

Next, in case we have parent-child relationship contexts data to child context can come from the persistent store or the parent context and when func save() throws is called on child context data is saved only 1 level up so it will be saved only to the parent context and won't go to the persistent store. To save it to the storage you need to call the save function on the parent context as well. Let's see how the initialisation of these contexts looks in the code:

let mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
mainContext.persistentStoreCoordinator = coordinator
mainContext.mergePolicy = NSMergePolicy.rollback
mainContext.undoManager = nil

let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = mainContext
backgroundContext.mergePolicy = NSMergePolicy.rollback
backgroundContext.undoManager = nil

This approach is really helpful when we need to work with some temporary data or need to implement the ability to cancel some changes. We can create a child context to allow users of our app to make any changes with data on it. In case these changes were cancelled we can just remove this child context without any effect on the persistent store. And if the user wants to save them we need to ensure that we call the save function on each context in the hierarchy. But the minus of this approach is that any fetch request made on the child context will always return fault objects even if returnsObjectsAsFaults is set to false. This affects performance a lot when we are processing large amounts of data and need to fire faults.

0
Subscribe to my newsletter

Read articles from Denis Mukha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Denis Mukha
Denis Mukha

iOS developer. Started developing in Objective-C when iOS 4 was introduced. Now I'm using Swift most of the time but Objective-C still takes quite a big part of my heart =). Before I moved to the Apple world I mainly developed in C#. Currently I'm a Head of iOS development in Mercaux.