How To Observe A Managed Object Context

How To Observe A Managed Object Context

Have you ever wondered how the NSFetchedResultsController class does its magic? A fetched results controller seems to be magically notified whenever the result of its fetch request changes. The Core Data framework exposes several APIs that allow developers to replicate the behavior of the NSFetchedResultsController class.

In this tutorial, I show you how to leverage these APIs to observe a managed object context and be notified whenever an event takes place that your application is interested in.

Notifications

The Core Data framework uses notifications to notify objects of changes taking place in a managed object context. Every managed object context posts three types of notifications to notify objects about the changes taking place in the managed object context:

  • NSManagedObjectContextObjectsDidChangeNotification
  • NSManagedObjectContextWillSaveNotification
  • NSManagedObjectContextDidSaveNotification

Managed Object Context Did Change

The NSManagedObjectContextObjectsDidChangeNotification notification is broadcast every time a managed object in the managed object context changes. Every time a managed object is inserted, updated, or deleted from a managed object context, the managed object context posts a NSManagedObjectContextObjectsDidChangeNotification notification.

Managed Object Context Will Save

As the name of the NSManagedObjectContextWillSaveNotification notification suggests, this notification is posted before a save operation is performed.

Managed Object Context Did Save

The managed object context performing the save operation posts a NSManagedObjectContextDidSaveNotification notification after successfully saving its changes.

Observing Notifications

Adding an object as an observer for Core Data notifications is straightforward. In the example below, a view controller monitors the managed object context it has a reference to.

import UIKit
import CoreData

class ViewController: UITableViewController {

    var managedObjectContext: NSManagedObjectContext?

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        if let managedObjectContext = managedObjectContext {
            // Add Observer
            let notificationCenter = NSNotificationCenter.defaultCenter()
            notificationCenter.addObserver(self, selector: #selector(managedObjectContextObjectsDidChange), name: NSManagedObjectContextObjectsDidChangeNotification, object: managedObjectContext)
            notificationCenter.addObserver(self, selector: #selector(managedObjectContextWillSave), name: NSManagedObjectContextWillSaveNotification, object: managedObjectContext)
            notificationCenter.addObserver(self, selector: #selector(managedObjectContextDidSave), name: NSManagedObjectContextDidSaveNotification, object: managedObjectContext)
        }
    }

}

Note that the view controller specifically monitors the managed object context it has a reference to. If you pass nil as the last argument of addObserver(_:selector:name:object:), the view controller receives notifications of every managed object context created by the application. While this may seem convenient, this can be pretty overwhelming if you are working with a complex Core Data stack. In most scenarios, it is recommended to monitor a specific managed object context.

Handling Notifications

It is up to the object observing the managed object context to decide what to do with the information it receives. Let me first show you what that information looks like.

The object property of the notification is the managed object context posting the notification. If an object is observing multiple managed object contexts, you can inspect the object property to find out which managed object context posted the notification.

The information we are interested in is stored in the userInfo dictionary of the notification object. The userInfo dictionary includes three important keys:

  • NSInsertedObjectsKey
  • NSUpdatedObjectsKey
  • NSDeletedObjectsKey

The value of each of these keys corresponds with a set of managed objects. These sets contain the managed objects that were inserted into, updated, and deleted from the managed object context that posted the notification.

func managedObjectContextObjectsDidChange(notification: NSNotification) {
    guard let userInfo = notification.userInfo else { return }

    if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> where inserts.count > 0 {

    }

    if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> where updates.count > 0 {

    }

    if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> where deletes.count > 0 {

    }
}

As I mentioned earlier, it is up to the observer to decide what to do with the information included in the notification. It is important to emphasize that some notifications can be sent very frequently. The NSManagedObjectContextObjectsDidChangeNotification notification, for example, is sent every time a managed object is modified.

The frequency with which NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification notifications are posted depends on the application's Core Data implementation. Most of the Core Data applications I write, perform a save operation when the application enters the background or when it is about to be terminated. I explain this in more detail in Building the Perfect Core Data Stack.

Monitoring Changes

It is important to understand what triggers the notifications we discussed earlier. While Core Data is a performant framework, you need to make sure handling notifications doesn't slow down your application. If you are performing complex operations every time a managed object is modified, you may run into performance issues.

An Example

I have created a basic application to illustrate what you can do with the information a managed object context sends its observers. To follow along, clone or download the project from GitHub and open it in Xcode. Build the application to make sure everything is working.

Monitoring Updates

The example application has the ability to create notes and link them to the current user. A user can have many notes and a note is always associated with one user.

The method we are interested in is managedObjectContextObjectsDidChange(_:) in the ViewController class. The ViewController class observes the managed object context it has a reference to and every time a managed object is modified in the managed object context, the managedObjectContextObjectsDidChange(_:) method is invoked.

func managedObjectContextObjectsDidChange(notification: NSNotification) {
    guard let userInfo = notification.userInfo else { return }

    if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> where inserts.count > 0 {
        print("--- INSERTS ---")
        print(inserts)
        print("+++++++++++++++")
    }

    if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> where updates.count > 0 {
        print("--- UPDATES ---")
        for update in updates {
            print(update.changedValues())
        }
        print("+++++++++++++++")
    }

    if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> where deletes.count > 0 {
        print("--- DELETES ---")
        print(deletes)
        print("+++++++++++++++")
    }
}

In this method, we inspect the userInfo dictionary of the notification object and print every managed object that is inserted into and deleted from the managed object context. For updates, we print the properties and values that were modified. The changedValues() method returns a dictionary that contains the names of the properties that were modified, including the old value of the property.

Run the application in the simulator, tap the Profile button in the top left, and modify the first name of the user. When you tap the Save button at the bottom, the managed object is updated and a notification is posted by the managed object context. This is what you should see in Xcode's console.

--- UPDATES ---
["first": Jim]
+++++++++++++++

As you can see, the changedValues() method is very convenient for understanding which properties were modified.

Monitoring Inserts

Add a new note to the user by tapping the + button in the top right. Enter a title in the text field and some content in the text view. Tap the Save button to save the note.

You may be surprised by the output in the console. Even though we inserted a note into the managed object context, the NSManagedObjectContextObjectsDidChangeNotification notification also includes information about a managed object being updated.

--- INSERTS ---
[<CoreDataNotifications.Note: 0x134703bb0> (entity: Note; id: 0x134714580 <x-coredata:///Note/tDF76BFDF-5A32-422D-8FF5-B5D5D9C4AE922> ; data: {
    content = "A note to myself ...";
    createdAt = nil;
    title = Note;
    updatedAt = nil;
    user = "0xd000000000040000 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/User/p1>";
})]
+++++++++++++++
--- UPDATES ---
["notes": {(
    <CoreDataNotifications.Note: 0x134575fb0> (entity: Note; id: 0xd000000000040002 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/Note/p1> ; data: {
    content = "Some text ...";
    createdAt = nil;
    title = Title;
    updatedAt = nil;
    user = "0xd000000000040000 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/User/p1>";
}),
    <CoreDataNotifications.Note: 0x134703bb0> (entity: Note; id: 0x134714580 <x-coredata:///Note/tDF76BFDF-5A32-422D-8FF5-B5D5D9C4AE922> ; data: {
    content = "A note to myself ...";
    createdAt = nil;
    title = Note;
    updatedAt = nil;
    user = "0xd000000000040000 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/User/p1>";
})
)}]
+++++++++++++++

Taking a closer look should clarify what is happening. The set of managed objects inserted into the managed object context contains the note we added. That is to be expected.

--- INSERTS ---
[<CoreDataNotifications.Note: 0x134703bb0> (entity: Note; id: 0x134714580 <x-coredata:///Note/tDF76BFDF-5A32-422D-8FF5-B5D5D9C4AE922> ; data: {
    content = "A note to myself ...";
    createdAt = nil;
    title = Note;
    updatedAt = nil;
    user = "0xd000000000040000 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/User/p1>";
})]
+++++++++++++++

The set of updated managed objects includes the user record to which the note was added. By setting the user property of the new note to the current user, Core Data automatically added the note to the set of notes of the user. This is reflected in the output in the console.

--- UPDATES ---
["notes": {(
    <CoreDataNotifications.Note: 0x134575fb0> (entity: Note; id: 0xd000000000040002 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/Note/p1> ; data: {
    content = "Some text ...";
    createdAt = nil;
    title = Title;
    updatedAt = nil;
    user = "0xd000000000040000 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/User/p1>";
}),
    <CoreDataNotifications.Note: 0x134703bb0> (entity: Note; id: 0x134714580 <x-coredata:///Note/tDF76BFDF-5A32-422D-8FF5-B5D5D9C4AE922> ; data: {
    content = "A note to myself ...";
    createdAt = nil;
    title = Note;
    updatedAt = nil;
    user = "0xd000000000040000 <x-coredata://2EB8E74B-425B-44C0-BE4C-DF8D946D4C6A/User/p1>";
})
)}]
+++++++++++++++

Remember that we only print the properties that were updated. The output shows us that the notes property was updated. There are currently two notes associated with the user.

Changed Values

There is one more thing I would like to show you. Stop the application to start with a clean slate. Run the application one more time and tap the Profile button to modify the properties of the user. Change the first name to Han and tap the Save button. This is what you should see in the console.

--- UPDATES ---
["first": Han]
+++++++++++++++

Tap the Profile button again and change the last name to Solo. Tap the Save button to update the user record. The output should now look like this.

--- UPDATES ---
["first": Han, "last": Solo]
+++++++++++++++

The changedValues() method returns a dictionary that includes every property of the user record that has been modified since the record was fetched or since the record was last saved. In other words, Core Data coalesces the changes for us.

This is not always what you want, though. Fortunately, Core Data also provides an API that only lists the properties that were modified since the last NSManagedObjectContextObjectsDidChangeNotification notification was posted, changedValuesForCurrentEvent().

What's Next?

Core Data offers a range of APIs for monitoring managed objects. This enables an application to stay up to date about the state of its object graph. The notification types we discussed in this tutorial are very useful for a variety of common tasks, such as keeping the user interface synchronized with the object graph managed by Core Data.

Download the example project from GitHub to take a look at the sample application. Questions? Leave them in the comments below or reach out to me on Twitter.