Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top