Responding to changes in a managed object context
Published on: November 23, 2020Working with multiple managed object contexts will often involve responding to changes that were made in one context to update another context. You might not even want to update another context but reload your UI or perform some other kind of update. Maybe you want to do this when a specific context updates, or maybe you want to run some code when any context updates.
In this week's post I will show you how you can listen for changed in managed object contexts, and how you can best use them. I will also show you a convenient way to extract information from a Core Data related Notification
object through a nice extension.
Subscribing to Core Data related Notifications
Regardless of your specific needs, Core Data has a mechanism that allows you to be notified when a managed object updates. This mechanism plays a key role in objects like NSFetchedResultsController
which tracks a specific managed object context in order to figure out whether specific objects were inserted, deleted or updated. In addition to this, a fetch result also tracks whether the position of a managed object within a result set has changed which is not something that you can trivially track yourself; this is implemented within the fetched results controller.
You can monitor and respond to changes in your managed object contexts through NotificationCenter
. When your managed object context updates or saves, Core Data will post a notification to the default NotificationCenter
object.
For example, you can listen for an NSManagedObjectContext.didSaveObjectsNotification
to be notified when a managed object context was saved:
class ExampleViewModel {
init() {
let didSaveNotification = NSManagedObjectContext.didSaveObjectsNotification
NotificationCenter.default.addObserver(self, selector: #selector(didSave(_:)),
name: didSaveNotification, object: nil)
}
@objc func didSave(_ notification: Notification) {
// handle the save notification
}
}
The example code above shows how you can be notified when any managed object context is saved. The notification you receive here contains a userInfo
dictionary that will tell you which objects were inserted, deleted and/or updated. For example, the following code extracts the inserted objects from the userInfo
dictionary:
@objc func didSave(_ notification: Notification) {
// handle the save notification
let insertedObjectsKey = NSManagedObjectContext.NotificationKey.insertedObjects.rawValue
print(notification.userInfo?[insertedObjectsKey])
}
Note that NSManagedObjectContext
has a nested type called NotificationKey
. This type is an enum
that has cases for every relevant key that you might want to use. Since the enum
case name for the notification keys don't match with the string that you need to access the relevant key in the dictionary, it's important that you use the enum's rawValue
rather than the enum case directly.
Note that
NSManagedObjectContext.NotificationKey
is only available on iOS 14.0 and up. For iOS 13.0 and below you can use theNotification.Name.NSManagedObjectContextDidSave
to listen for save events. For a more complete list for iOS 13.0 notifications I'd like to point you to the "See Also" section on the documentation page forNSManagedObjectContextDidSave
which is located here.
I'm not a big fan of how verbose this is so I like to use an extension on Dictionary
to help me out:
extension Dictionary where Key == AnyHashable {
func value<T>(for key: NSManagedObjectContext.NotificationKey) -> T? {
return self[key.rawValue] as? T
}
}
This extension is very simple but it allows me to write code from before as follows which is much cleaner:
@objc func didSave(_ notification: Notification) {
// handle the save notification
let inserted: Set<NSManagedObject>? = notification.userInfo?.value(for: .insertedObjects)
print(inserted)
}
We could take this even further with an extension on Notfication
specifically for Core Data related notifications:
extension Notification {
var insertedObjects: Set<NSManagedObject>? {
return userInfo?.value(for: .insertedObjects)
}
}
This notification would be used as follows:
@objc func didSave(_ notification: Notification) {
// handle the save notification
let inserted = notification.insertedObjects
print(inserted)
}
I like how clean the callsite is here . The main downside is that we can't constrain the extension to Core Data related notifications only, and we'll need to manually add computed properties for every notification key. For example, to extract all updated objects through a Notification
extension you'd have to add the following property to the extension I showed you earlier:
var updatedObjects: Set<NSManagedObject>? {
return userInfo?.value(for: .updatedObjects)
}
It's not a big deal to add these computed properties manually, and it can clean up your code quite a bit so it's worth the effort in my opinion. Whether you want to use an extension like this is really a matter of preference so I'll leave it up to you to decide whether you think this is a good idea or not.
Let's get bakc on topic, this isn't a section about building convenient extensions after all. It's about observing managed object context changes.
The code I showed you earlier subscribed to the NSManagedObjectContext.didSaveObjectsNotification
in a way that would notify you every time any managed object context would save. You can constrain this to a specific notification as follows:
let didSaveNotification = NSManagedObjectContext.didSaveObjectsNotification
let targetContext = persistentContainer.viewContext
NotificationCenter.default.addObserver(self, selector: #selector(didSave(_:)),
name: didSaveNotification, object: targetContext)
By passing a reference to a managed object context you can make sure that you're only notified when a specific managed object context was saved.
Imagine that you have two managed object contexts. A viewContext
and a background context. You want to update your UI whenever one of your background contexts saves, triggering a change in your viewContext
. You could subscribe to all managed object context did save notifications and simply update your UI when any context got saved.
This would work fine if you have set automaticallyMergesChangesFromParent
on your viewContext
to true
. However, if you've set this property to false
you find that your viewContext
might not yet have merged in the changes from the background context which means that updating your UI will not always show the lastest data.
You can make sure that a managed object context merges changes from another managed object context by subscribing to the didSaveObjectsNotification
and merging in any changes that are contained in the received notification as follows:
@objc func didSave(_ notification: Notification) {
persistentContainer.viewContext.mergeChanges(fromContextDidSave: notification)
}
Calling mergeChanges
on a managed object context will automatically refresh any managed objects that have changed. This ensures that your context always contains all the latest information. Note that you don't have to call mergeChanges
on a viewContext
when you set its automaticallyMergesChangesFromParent
property to true
. In that case, Core Data will handle the merge on your behalf.
In addition to knowing when a managed object context has saved, you might also be interested in when its objects changed. For example, because the managed object merged in changes that were made in another context. If this is what you're looking for, you can subscribe to the didChangeObjectsNotification
.
This notification has all the same characteristics as didSaveObjectsNotification
except it's fired when a context's objects change. For example when it merges in changes from another context.
The notifications that I've shown you so far always contain managed objects in their userInfo
dictionary, this provides you full access to the changed objects as long as you access these objects from the correct managed object context.
This means that if you receive a didSaveObjectsNotification
because a context got saved, you can only access the included managed objects on the context that generated the notification. You could manage this by extracting the appropriate context from the notifiaction as follows:
@objc func didSave(_ notification: Notification) {
guard let context = notification.object as? NSManagedObjectContext,
let insertedObjects = notification.insertedObjects as? Set<ToDoItem> else {
return
}
context.perform {
for object in insertedObjects {
print(object.dueDate)
}
}
}
While this works, it's not always appropriate.
For example, it could make perfect sense for you to want to access the inserted objects on a different managed object context for a variety of reasons.
Extracting managed object IDs from a notification
When you want to pass managed objects from a notification to a different context, you could of course extract the managed object IDs and pass them to a different context as follows:
@objc func didSave(_ notification: Notification) {
guard let insertedObjects = notification.insertedObjects else {
return
}
let objectIDs = insertedObjects.map(\.objectID)
for id in objectIDs {
if let object = try? persistentContainer.viewContext.existingObject(with: id) {
// use object in viewContext, for example to update your UI
}
}
}
This code works, but we can do better. In iOS 14 it's possible to subscribe to Core Data's notifications and only receive object IDs. For example, you could use the insertedObjectIDs
notification to obtain all new object IDs that were inserted.
The Notification
extension property to get convenient access to insertedObjectIDs
would look as follows:
extension Notification {
// other properties
var insertedObjectIDs: Set<NSManagedObjectID>? {
return userInfo?.value(for: .insertedObjectIDs)
}
}
You would then use the following code to extract managed object IDs from the notification and use them in your viewContext
:
@objc func didSave(_ notification: Notification) {
guard let objectIDs = insertedObjects.insertedObjectIDs else {
return
}
for id in objectIDs {
if let object = try? persistentContainer.viewContext.existingObject(with: id) {
// use object in viewContext, for example to update your UI
}
}
}
It doesn't save you a ton of code but I do like that this notification is more explicit in its intent than the version that contains full managed objects in its userInfo
.
In Summary
Notifications can be an incredibly useful tool when you're working with any number of managed object contexts, but I find them most useful when working with multiple managed object contexts. In most cases you'll be interested in the didChangeObjectsNotification
for the viewContext
only. The reason for this is that it's often most useful to know when your viewContext
has merged in data that may have originated in another context. Note that didChangeObjectsNotification
also fires when you save a context.
This means that when you subscribe to didChangeObjectsNotification
on the viewContext
and you insert new objects into the viewContext
and then call save()
, the didChangeObjectsNotification
for your viewContext
will fire.
When you use NSFetchedResultsController
or SwiftUI's @FetchRequest
you may not need to manually listen for notifications often. But it's good to know that these notifications exist, and to understand how you can use them in cases where you're doing more complex and custom work.
If you have any questions about this post, or if you have feedback for me you can reach out to me on Twitter.