views:

983

answers:

2

I have an NSView subclass which is bound to the arrangedObjects of an NSArrayController. When the array has an item inserted or removed the view is notified. How do I get it to be notified if a model stored in the array has an attribute changed?

Do I need to add my view as an observer to every (relevant) attribute of every item added to the array?

When an item is added to or removed from the array I am notified via observeValueForKeyPath:ofObject:change:context: in my NSView subclass. I am not notified of changes to the models stored in the array but I could, every time I am notified of an insertion, add the view as an observer to the new item's attributes. Is this the best way to do this?

I overrode addObserver for the model class so that I could see what happens and noticed that NSTableView columns bound to the arrangedObjects add themselves as observers to the appropriate attributes. Can this be made to happen automagically or do I set up the observations manually?

+2  A: 

Maybe rather than observing potentially many key value paths, why not have each object in the array post a notification when something has changed, then only one object needs to observe one notification instead of one object observing many key value paths.

EDIT:

Also, your arrayed objects could also respond to a class method called +keyPathsForValuesAffecting<key> where <key> is your key name. Here's an example: paymentDue is a key, which is affected when the values invoiceItems.count or paymentsMade have changed. When invoiceItems.count or paymentsMade has changed, anything bound to paymentDue is sent a notification.

+ (NSSet *) keyPathsForValuesAffectingPaymentDue:
{
    return [NSSet setWithObjects:@"invoiceItems.count", @"paymentMade", nil];
}

If you are running on 10.4, or are targeting 10.4 or earlier, you'll need to use this method instead, but it essentially boils down to the same thing.

EDIT 2:

To clarify your other comment; you can still have each object in the array manually call

[[NSNotificationCenter defaultCenter] postNotificationName:@"ModelDidChange" object:self];

Then, with some controller code you can register for notification updates from your objects. If you choose a unique notification name then you won't need to manually listen from specific objects, you can tell the NSNotificationCenter that you want to receive notifications from any object. Your controller can work out which object has changed quite easily.

  1. Register with the notification center (these methods should be in a controller object):

    // This string could really be just about anything you want, but make it conspicuous.
    static NSString * const ModelDidChangeName = @"ModelDidChange";
    
    
    - (void) awakeFromNib
    {
        // using nil for the object parameter will make the notification center
        // invoke modelDidChange: regardless of who the sender is.
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(modelDidChange:) name:ModelDidChangeName object:nil];
    }
    
  2. Implement a method to handle the notification.

    - (void) modelDidChange:(NSNotification *) notification
    {
        MyModelClass *myObject = [notification object];
        // do stuff with your model if necessary.
        // do stuff with your view too
    }
    
  3. Get your model objects to post notifications when parts of them change:

    @implementation MyModelClass
    
    
    - (void) setSomething:(NSString *) newThing
    {
        [something autorelease];
        something = [newThing copy];
        if (something == nil)
        {
            // special case scenario for when something is nil
            // do stuff, modify MyModelClass instance's attributes
            [[NSNotificationCenter defaultCenter] postNotificationName:ModelDidChange object:self];
            // the controller's modelDidChange: method is automatically invoked.
        }
    }
    @end
    

But

If your model is properly KVC compliant and made with the appropriate KVO callbacks, manual notifications won't be necessary.

dreamlax
That could work well for what I'm trying to do.Either way I have to observe each object in the array, right? For some reason I thought it would be possible for the models signal that a change had happened and then the NSArrayController could tell its observers.
toholio
Thanks for this. I'll read the documentation more carefully. I understand the notifications will be sent automatically for KVC compliant properties but I don't understand how something observing the array would get changes for a model in the array. keyPathsForValuesAffectingValueForKey would seem to only be for other properties of the same object, or have a I misunderstood? Sorry if I'm a bit dense.
toholio
AH, ok a bit more reading and I think I might understand now. I'll need to get home to check what I'm thinking works though. Basically, if I have a model with a KVC property 'name' then I could observe the keypath 'arrangedObjects.name' to get a notification each time one of the models has a name change? This might be a forehead slapping moment for me...
toholio
+1  A: 

A big thank you to dreamlax but I think I didn't do a good enough job explaining my problem. My model class was observable and produced the right notifications but I couldn't work out how to observe them without observing every item in the array directly.

I think the documentation for key paths could be improved because I couldn't find anything that explained the very simple change I needed to make. There's some good info the array magic keypaths but no simple "these are the common things" documentation.

Anyway. Previously in my NSView subclass I had the following:

- (void) bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
  if ([binding isEqualToString:@"observedObjects"]) {
    [observable addObserver:self forKeyPath:@"arrangedObjects" options:0 context:nil];
  } else {
    [super bind:binding toObject:observable withKeyPath:keyPath options:options];
  }
}

To get notification of changes to the models within the NSArrayController's arrangedObjects all I needed to add was observation of arrangedObjects.name (for the name property of my model). So the above code became:

- (void) bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
  if ([binding isEqualToString:@"observedObjects"]) {
    [observable addObserver:self forKeyPath:@"arrangedObjects" options:0 context:nil];
    [observable addObserver:self forKeyPath:@"arrangedObjects.name" options:0 context:nil];
  } else {
    [super bind:binding toObject:observable withKeyPath:keyPath options:options];
  }
}

That's it! Now if any object in arrangedObjects gets its name changed I am notified.

toholio