Cocoa Bindings – The Easy way for an NSView subclass to update itself

So, say you have a NSView subclass in your Interface that is purely data-driven (i.e. it’s not an interactive component, it merely provides a graphical representation of an underlying data model).  It would be nice to keep this view updated to the current state of your data model automatically without writing too much glue code.  Here comes Cocoa Bindings!

Now, as Cocoa Bindings and Core Data are both powerful technologies, it’s hard to write a tutorial that doesn’t start from the beginning.  So, this is not a tutorial (per se), nor am I starting from the beginning.  Here is the start situation:

– You are using Core Data in your app, and don’t have any issues like I have described in blog posts preceding these (i.e. pretty much every attribute is immutable (i.e. you use myAttribute = newAttribute to register any kind of change on my attribute),

– You are familiar with NSArrayControllers and know a bit about basic Cocoa Bindings.

So?  How to get things working?  The trick is to make use of the CoreData framework’s automatically generated NSNotifications, namely this one:

NSManagedObjectContextObjectsDidChangeNotification, which you can use to determine if any of these objects are of any interest to your NSView subclass, and if yes, tell your class to redraw itself.

How does this all begin?  It begins with the bindings.   In an app I’m working on, I have sort of objects that have a specific duration, and this duration is derived from the duration of child objects.  Imagine the parent object as a day, and the child objects as tasks, and those children have children of subtasks, who have a duration.  Get it ?  A sort of pseudo equation for the entire duration would be:

totalDuration = [day valueForKeyPath: @"tasks.@sum.subtasks.@sum.duration"];

(although in Cocoa this isn’t allowed, but perhaps you get the shorthand.  It sums all durations in the sub elements)

Or said another way, a Day has many Task object who each have many Subtask objects, and a subtask has a duration property that you are editing elsewhere in your GUI.

So, how do we get this working? Here would be a portion of your NSView subclass’ .m file. The .h is a subclass of NSView and has:

@property (nonatomic, strong) Day *day;

static int DayObservingContext; // see here for an explanation of KVO contexts http://stackoverflow.com/a/11917449/421797

+ (void)initialize
{
    [self exposeBinding:@"day"];
}

- (void)bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
    if ([binding isEqualToString:@"day"]) {
        [observable addObserver:self forKeyPath:@"selection" options:0 context:&DayObservingContext];
        _arrayController = (NSArrayController*)observable;
    }
    else{
        [super bind:binding toObject:observable withKeyPath:keyPath options:options];
    }
}

- (void)unbind:(NSString *)binding
{
    if ([binding isEqualToString:@"day"]) {

        [_arrayController removeObserver:self forKeyPath:@"selection"];
        _arrayController = nil;
        self.day = nil;
    }
    else{
        [super unbind: binding];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &DayObservingContext) {

        id selection = _arrayController.arrangedObjects[_arrayController.selectionIndex];

        if (!selection && [selection isKindOfClass:[Day class]] == NO) {
            [NSException raise:NSInternalInconsistencyException format:@"This object should be bound to Day Objects!"];
        }

        self.day = (Day*)selection;

        [self setNeedsDisplayInRect:[self visibleRect]];
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (Day*)day { return _day;}
- (void)setDay:(Day *)day
{
    if (day == _day) {
        return;
    }

    if (_day) {
        [[NSNotificationCenter defaultCenter] removeObserver: self
                                                        name: NSManagedObjectContextObjectsDidChangeNotification
                                                      object: _day.managedObjectContext];
    }

    _day = day;

    if (_day) {
        [[NSNotificationCenter defaultCenter] addObserver: self
                                                 selector: @selector(contextChanged:)
                                                     name: NSManagedObjectContextObjectsDidChangeNotification
                                                   object: _day.managedObjectContext];
    }

    [self setNeedsDisplayInRect:[self visibleRect]];
}

- (void)contextChanged:(NSNotification*)notification
{
    // Get a set containing ALL objects which have been changed
    NSSet* insertedObjects = (NSSet*)[[notification userInfo] objectForKey:NSInsertedObjectsKey];
    NSSet* updatedObjects = (NSSet*)[[notification userInfo] objectForKey:NSUpdatedObjectsKey];
    NSSet* deletedObjects = (NSSet*)[[notification userInfo] objectForKey:NSDeletedObjectsKey];

    NSSet *changedObjects = [NSSet set];

    if (insertedObjects) changedObjects = [changedObjects setByAddingObjectsFromSet:insertedObjects];
    if (updatedObjects) changedObjects = [changedObjects setByAddingObjectsFromSet:updatedObjects];
    if (deletedObjects) changedObjects = [changedObjects setByAddingObjectsFromSet: deletedObjects];

    // go through all objects, find type, and see if they have a relationship that is a part of this Day
    BOOL shouldRefresh = NO;

    for (NSManagedObject *object in changedObjects) {

        if ([object isKindOfClass:[Day class]]) {
            if (object == _day) {
                shouldRefresh = YES;
                break;
            }
        }
        if ([object isKindOfClass:[Task class]]) {
            if ([(SCRecipeStep*)object day] == _day) {
                shouldRefresh = YES;
                break;
            }
        }
        if ([object isKindOfClass:[Subtask class]]) {  // it is a subtask that has the .duration property which is edited elsewhere in your GUI
            if ([[(Subtask*)object task] day] == _day) {
                shouldRefresh = YES;
                break;
            }
        }
    }

    if (shouldRefresh) {
        [self setNeedsDisplayInRect:[self visibleRect]];
    }

}

- (void)drawRect:(NSRect)dirtyRect
{
   // your custom code here that knows how to draw a Day object
}

Now, all you have to do in Interface Builder is create an outlet to the NSArrayController that is managing your Day objects (i.e. _arrayController.arrangedObjects will be a collection of Day objects), and of course an outlet to your “DayView” subclass.

then, you just add the binding in your loadView method:

- (void)loadView
{
    [super loadView];

    [self.dayView bind: @"day"
              toObject: self.daysArrayController
           withKeyPath: @"selection" /* due to the way we overrode bind: we could specify nil here */
               options: nil];
}

And so any time your currently selected day changes, it will know how to change the currently drawn day, and because we are using NSNotificationCenter to be informed of any changes to the managedObjectContext, and have code that checks if any of these changes are relevant to our view, the DayView will update itself accordingly.

I spent ages trying to get the Day object’s children to notify the parent if any of its properties change (explained in the KVO documentation, “Registering Dependent Keys, to-many relationships), but found it to be an insane amount of code required and thought “there must be an easier way than this”, I read that one sentence in this section of the documentation that states:

“2. If you’re using Core Data, you can register the parent with the application’s notification center as an observer of its managed object context. The parentshould respond to relevant change notifications posted by the children in a manner similar to that for key-value observing.”

This is what this post attempts to clarify. I hope it helps. I can’t believe how much time I’ve spent trying to get something that’s meant to be simple, working.

PS – THIS POST was very helpful for getting the right approach

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s