Since the introduction of the NSFetchedResultsController, I’ve run into numerous situations in which I wanted to use its ability to detect data changes even when I wasn’t using a UITableView. Building a user interface for an iPad where three different entities are being displayed at once is a situation that begs for some kind of watcher to notify the user interface when the data has changed. Needing to watch not just a single type of entity but also relationships associated with that entity is another place where an observer is useful. Frequently I would attempt to use an NSFetchedResultsController and run into one problem or another that made it more difficult than it needed to be. This led me to investigate how the NSFetchedResultsController worked and finally resulted in the creation of the ZSContextWatcher.
The ZSContextWatcher is publicly available under the BSD license, and the latest version is always in my public GitHub repository at http://github.com/mzarra/ZDS_Shared.
The goal of the ZSContextWatcher is to provide us with the ability to monitor a subset of the data that’s in Core Data and to be notified when it changes. It’s the same functionality that’s in the NSFetchedResultsController but not as tightly coupled with the UITableView.
| @interface ZSContextWatcher : NSObject |
| - (id)initWithManagedObjectContext:(NSManagedObjectContext*)context; |
| - (void)addEntityToWatch:(NSEntityDescription*)description |
| withPredicate:(NSPredicate*)predicate; |
| @end |
The API to use this class is composed of an initializer and two methods.
We initialize the ZSContextWatcher with an NSManagedObjectContext. This NSManagedObjectContext is used when it sets itself up as an observer on NSNotificationCenter. This avoids notifications coming from other NSManagedObjectContext instances.
| - (id)initWithManagedObjectContext:(NSManagedObjectContext*)context; |
| { |
| ZAssert(context, @"Context is nil!"); |
| if (!(self = [super init])) return nil; |
| |
| NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; |
| [center addObserver:self |
| selector:@selector(contextUpdated:) |
| name:NSManagedObjectContextDidSaveNotification |
| object:context]; |
| |
| return self; |
| } |
The second method in the public API for the ZSContextWatcher allows us to define what the watcher is listening for. This is moved away from the initialization because I wanted the ability to watch more than one entity and/or more than one predicate. With this method, I can add as many entities and/or predicates as I need.
| - (void)addEntityToWatch:(NSEntityDescription*)description |
| withPredicate:(NSPredicate*)predicate; |
| { |
| NSPredicate *entityPredicate = nil; |
| NSPredicate *final = nil; |
| NSArray *array = nil; |
| entityPredicate = [NSPredicate predicateWithFormat:@"entity.name == %@", |
| [description name]]; |
| array = [NSArray arrayWithObjects:entityPredicate, predicate, nil]; |
| final = [NSCompoundPredicate andPredicateWithSubpredicates:array]; |
| |
| if (![self masterPredicate]) { |
| [self setMasterPredicate:finalPredicate]; |
| return; |
| } |
| |
| array = [NSArray arrayWithObjects:[self masterPredicate], final, nil]; |
| finalPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:array]; |
| [self setMasterPredicate:finalPredicate]; |
| } |
We do a bit of NSPredicate construction in the implementation. First, we take the passed-in NSEntityDescription and use that inside a new predicate that compares the entity name. Next, we create a compound predicate that combines the passed-in predicate with the newly created entity predicate with an AND join. Now we have a new predicate that checks to make sure the compared object is the same entity before we use the second part of the predicate against the object.
Why do we do this? If we just run the passed-in predicate against every object, we’d get an error when it hits an object that doesn’t have one of the properties in the predicate. By adding a prefix predicate that checks the name of the entity, we’re ensuring it will run only against the correct entity.
If there’s no existing predicate in our ZSContextWatcher, we set our new compound predicate as the masterPredicate and return. However, if there’s already a masterPredicate set, we must compound the existing predicate with our new one. Again, we use an NSCompoundPredicate to combine the existing masterPredicate and our new predicate. However, this time we use an OR instead of an AND in the compound predicate. Finally, we take the newly created compound predicate and set that as the masterPredicate.
We’ve constructed a predicate that we can run against a collection of NSManagedObject instances, and it will filter out any objects that we don’t care about. Now when we receive a notification from an NSManagedObjectContextDidSaveNotification, we can easily filter the incoming objects against our predicate.
| - (void)contextUpdated:(NSNotification*)notification |
| { |
| NSInteger totalCount = 0; |
| NSSet *temp = nil; |
| temp = [[notification userInfo] objectForKey:NSInsertedObjectsKey] |
| NSMutableSet *inserted = [temp mutableCopy]; |
| if ([self masterPredicate]) { |
| [inserted filterUsingPredicate:[self masterPredicate]]; |
| } |
| totalCount += [inserted count]; |
| |
| temp = [[notification userInfo] objectForKey:NSDeletedObjectsKey]; |
| NSMutableSet *deleted = [temp mutableCopy]; |
| if ([self masterPredicate]) { |
| [deleted filterUsingPredicate:[self masterPredicate]]; |
| } |
| totalCount += [deleted count]; |
| |
| temp = [[notification userInfo] objectForKey:NSUpdatedObjectsKey]; |
| NSMutableSet *updated = [temp mutableCopy]; |
| if ([self masterPredicate]) { |
| [updated filterUsingPredicate:[self masterPredicate]]; |
| } |
| totalCount += [updated count]; |
| |
| if (totalCount == 0) { |
| return; |
| } |
| |
| NSMutableDictionary *results = [NSMutableDictionary dictionary]; |
| if (inserted) { |
| [results setObject:inserted forKey:NSInsertedObjectsKey]; |
| } |
| if (deleted) { |
| [results setObject:deleted forKey:NSDeletedObjectsKey]; |
| } |
| if (updated) { |
| [results setObject:updated forKey:NSUpdatedObjectsKey]; |
| } |
| |
| if ([[self delegate] respondsToSelector:[self action]]) { |
| [[self delegate] performSelectorOnMainThread:[self action] |
| withObject:self |
| waitUntilDone:YES]; |
| } |
| } |
When we receive a notification, we need to check the ‑userInfo and see whether there are any objects that we care about. In the ‑userInfo, there are up to three NSSet instances: one for updated objects, one for deleted objects, and one for inserted objects. We walk through each of these sets, grabbing a mutable copy of each one, filtering the mutable set against our masterPredicate, and keeping track of how many objects are left. If no objects are left at the end of the filtering, we know there were none in the save that we cared about, and we can return.
If any objects are left, we have to notify our delegate about them. Since we’ve already filtered the objects, we may as well pass them to our delegate so our delegate doesn’t need to repeat the work. We create a new NSDictionary and add each of our NSSet instances to it using the same keys that the incoming NSNotification used. Once it’s constructed, we can pass the newly created NSDictionary off to our delegate.
Now we have a class that allows us to watch any NSManagedObjectContext of our choosing and notifies us if an object that we care about has been touched in any way. We can make the predicate as broad or narrow as we want. By allowing the delegate to pass in the predicate, we’ve also made this class highly reusable.