Home Cocoa Snippets Cocoa Notes Observing Arrays
Observing Arrays
Cocoa bindings allows you to link e.g. a NSTableView to a model data structure. When I want to implement this capability in my custom view the view needs to use key-value coding (KVC) and key-value observing (KVO). KVC is used to modify properties of the model the view is linked to and KVO will keep the view informed about changes of the model.
Things start to become somewhat more complicated, when the view should observe some properties of a "bunch" of objects – so I tried to put this functionality in a generic helper class.

SFArrayObserver helper class 

In this article I'll focus on observing NSArray containers. I use a helper class 'SFArrayObserver' to make this easier. The source code can be found here. This is work in progress – feedback is welcome.
But let me start with an illustrative example. My custom view displays a calendar of events. The 'Event' objects are stored in an relation property of the document (which ultimately is an NSArray). Parts of the view need to be redrawn if an event is added to or removed from the array or if a relevant property of an event changes (time, title, color, etc.). The SFArrayObserver class observes both the array and selected properties of each Event in the array. Cange notifications are passed to the view through a set of callbacks:
- (void) observer:(SFArrayObserver*)observer
removeItemNotification:(id)item;
- (void) observer:(SFArrayObserver*)observer 
addItemNotification:(id)item;
- (void) observer:(SFArrayObserver*)observer
willChangeProperty:(NSString*)property ofItem:(id)item;
- (void) observer:(SFArrayObserver*)observer
didChangeProperty:(NSString*)property ofItem:(id)item;

For each of these callbacks, the view essentially just tells the window manager to update the 'affected area' using 'setNeedsDisplayInRect:'. Invalidating the area in the 'willChangeProperty' callback may only be needed for properties that modify the 'affected area' of an array element.

Here is an example how the array observer helper is configured and installed. To be able to create a master-detail interface in Interface Builder I added an IBOutlet 'controller' to my view which connects the view to the 'master' NSArrayController. The initialisation code shown below is placed in the 'awakeFromNib' method of the view. The resulting behaviour is like if I could bind my view to the controllers selection in Interface Builder: the view displays the 'schedule' of the item currently selected in the master view (multiple selections are not supported).

SFArrayObserver* updateObserver = [[SFArrayObserver alloc] init];
updateObserver.observedPropertyKeys = [NSArray arrayWithObjects:
@"title", @"startTime", @"duration", nil];
[updateObserver observeArrayKey:@"selection.schedule"
ofObject:controller forClient:self];
The resulting behaviour is like if I could bind my view to the controllers selection in Interface Builder: the view displays the 'schedule' of the item currently selected in the master view (multiple selections are not supported though). 
Now that all changes to the 'Event' objects cause the view to be updated, the event handlers of the view need no longer worry about redrawing: they can simply modify the events – the updating happens automatically.

Some notes on the implementation

When registering as observer you can specify a set of options that cause additional notifications (initial or prior to the change) or add information the the 'change' dictionary passed to the 'observeValueForKeyPath:ofObject:change:context' notification call. Unfortunately these options have no effect when observing through a NSController (as of Leopard). To work around this limitation the array is cached and changes of the array are detected by comparing the old and new array. The individual element properties are observed directly and will therefore generate prior notifications.

Download the initial version here (please report corrections and suggestions to stefan at infoatelier dot com).

 
Comments (1)Add Comment
ArrayObserver
written by chain , August 25, 2009
Have you had any ideas on how to find out which object was inserted into array, inside observeValueForKeyPath: ?

Write comment

busy