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.