14
Oct
2011
 

Parent Watching Its Child

by Marcus Zarra

Recently on StackOverflow I have seen several questions regarding the desire for a parent Entity to be updated whenever an attribute within the child has changed.

There are several different ways to solve this problem. The easiest is to have the child ping the parent whenever it changes and then the parent can update any values it needs to as a result of that ping. Another solution that I considered was to have the parent observe the property on each child. Of course this requires additional code to handle when a child is removed from the parent and when a new child is added to the parent. Lots of code to manage and the potential for fragility is rather high.

The most interesting solution that I offered was to have the parent watch for change notifications and react to them. It is this solution that I will discuss in a little more depth.

Change Notifications

Core Data produces several different notifications during its lifecycle. The most commonly used notification is NSManagedObjectContextDidSaveNotification. Whenever a NSManagedObjectContext saves it will fire two notifications, one before the save and one after the save. The one before the save does not contain any direct information about what is going to be saved although you can discover it yourself.

The second one, NSManagedObjectContextDidSaveNotification contains a reference to each entity that it saved; whether they are newly inserted entities, updated entities or deleted entities.

There is another notification that is not used nearly as often; NSManagedObjectContextObjectsDidChangeNotification. This notification is significantly more chatty than the other two. It potentially can fire every time an entity changes. More specifically it will fire at the end of each run loop where an entity has changed. While this notification is more chatty, it is more useful for this purpose.

Why use NSManagedObjectContextObjectsDidChangeNotification?

Of the three, the NSManagedObjectContextObjectsDidChangeNotification is the most useful for a parent/child monitoring. Both NSManagedObjectContextDidSaveNotification and NSManagedObjectContextWillSaveNotification only fire when a NSManagedObjectContext has been asked to save. A save can be a very rare event; possibly only once during the life of the application.

However, NSManagedObjectContextObjectsDidChangeNotification fires frequently enough that we can use it without having to worry about when a save is going to occur. We can use it during the life of the application and keep a parent updated to the status of its children.

An Example

A common example that I like to use while I am testing these theories is my very simple RSS reader. Those who have watched my Core Data videos are familiar with Zeader.

In Zeader, there are only two entities:

In this example we want the Server entity to be updated whenever the read attribute on a child has changed. To do that we are going to make a few enhancements to the Server entity subclass.

-awakeFromInsert and -awakeFromFetch

The first change we need to make is with the awake methods. Whenever a Server entity is created or loaded we need to start listening for NSManagedObjectContextObjectsDidChangeNotification postings.

- (void)awakeFromInsert
{
  [super awakeFromInsert];

  NSString *name = NSManagedObjectContextObjectsDidChangeNotification;
  NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  [center addObserver:self 
             selector:@selector(changes:) 
                 name:name 
               object:nil];
}

- (void)awakeFromFetch
{
  [super awakeFromFetch];

  NSString *name = NSManagedObjectContextObjectsDidChangeNotification;
  NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  [center addObserver:self 
             selector:@selector(changes:) 
                 name:name 
               object:nil];
}

-willTurnInfoFault

To be clean we need to stop listening whenever the Server is about to be removed from memory. Since it is inappropriate to override the -dealloc method of a NSManagedObject we need to do so in the -willTurnIntoFault method.

- (void)willTurnIntoFault
{
  [super willTurnIntoFault];
  NSString *name = NSManagedObjectContextObjectsDidChangeNotification;
  NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  [center removeObserver:self 
                    name:name 
                  object:nil];
}

This method of course, just like the two awake methods previously can be rolled up into just two lines. They are broken apart here to make it easier to consume on the web.

-changes:

The last piece to the puzzle is the need to react to those changes as they are posted.

- (void)changes:(NSNotification*)notification
{
  NSSet *objects = nil;
  NSMutableSet *combinedSet = nil;
  NSPredicate *predicate = nil;
  NSSet *unreadItems = nil;

  NSDictionary *userInfo = [notification userInfo];

  objects = [userInfo valueForKey:NSInsertedObjectsKey];
  combinedSet = [NSMutableSet setWithSet:objects];

  objects = [[notification userInfo] valueForKey:NSUpdatedObjectsKey];
  [combinedSet unionSet:objects];

  objects = [[notification userInfo] valueForKey:NSDeletedObjectsKey];
  [combinedSet unionSet:objects];

  predicate = [NSPredicate predicateWithFormat:@"entity.name == %@ && server == %@", 
              @"FeedItem", self];
  [combinedSet filterUsingPredicate:predicate];

  if ([combinedSet count] == 0) {
    return;
  }

  predicate = [NSPredicate predicateWithFormat:@"read == NO"];
  unreadItems = [[self feedItems] filteredSetUsingPredicate:predicate];
  [self setUnreadCount:[unreadItems count]];
  DLog(@"server status changed %i", [unreadItems count]);
}

First, we do not really care whether the entities are inserted, deleted or updated. We just need an answer to a simpler question: Are any of the entities that I care about in this change set? Since our question is easier, we first can lump all of the changes into a single set.

Once we have all of the changes in a single set we then filter that set on the important question. The predicate needs to be in a specific order; first we filter on the type of entity (FeedItem) we care about and then we filter on its relationship (is its Server me?).

This predicate reduces the NSMutableSet to only those FeedItem entities that are children of this instance of Server. If that resulting NSMutableSet is zero then there are no relevant changes and we return; done.

If there are changes we want to update our unreadCount. To do that we grab all of our feedItems and filter them with a simpler predicate of @"read == NO". The result of that filter can then be plugged into our unreadCount.

Conclusion

There are a few potential performance hot spots in this example. However, the monitoring of children by the parent is always going to be a performance hotspot. Therefore, great care should be given whenever to adding a watcher like this.

Marcus Zarra

Marcus S. Zarra is a founding partner of MartianCraft, LLC. He has been developing Cocoa software since 2003, Java software since 1996, and has been in the industry since 1985. Currently Marcus is producing software for iOS and OS X. In addition to writing software, he assists other developers by blogging about development and supplying code samples on Cocoa Is My Girlfriend. Marcus is also the author of Core Data (2nd edition): Data Storage and Management for iOS, OS X, and iCloud and Co-Author of Core Animation: Simplified Animation Techniques for Mac and iPhone Development. You can find Marcus on Twitter, on App.net and on StackOverflow.

More Posts - Website

Follow Me:
Twitter