How to work with a bound-to array

2008-11-26 00:19:05 -08:00

You have a model object with an array property:

@property NSArray *framistans;

You also have an array controller whose contentArray binding is bound to this property.

How do you add an object to the array?

Wrong #1: Direct manipulation

[framistans addObject:newFramistan];

The array controller will not notice this change. Worse, the value of your array no longer matches the value your array controller last saw. (For one thing, your array is now one object longer.)

Wrong #2: Use the array controller’s add: action

[arrayController add:nil];

This works, but now you need to go looking for that object. The proper way would be to get the binding info for the array controller’s contentArray binding, then ask the bound-to object for its array, and get the lastObject of that array.

More likely, you’ll hard-code knowledge of which object and which property it’s bound to. Good luck when you change the binding!

Oh, and in a model object, this breaks the separation between your model and everything else. Your model should know nothing of your UI, so that you can replace the UI wholesale if ever you want to (for example, if you make a CLI or web-based version of your app) and keep the same model.

Wrong #3: Add the object to the array controller’s content array

newFramistan = [arrayController newObject];
[arrayController addObject:newFramistan];

This works, and at least you already have the object on hand. But you’re still giving your model knowledge of the array controller, so you’re still breaking separation.

Almost right: Add it to the mutableArrayValue for your property

[[self mutableArrayValueForKey:@"framistans"] addObject:newFramistan];

This works, and it’s clean.

The only problem with it is that it’s too heavyweight: you’re creating this proxy object just to mutate a property. The most appropriate use for mutableArrayValueForKey: is if you want to pass a mutable array to another object, and you want that array to actually be a property of another object. This is a very rare case—I’ve never needed to do it.

For this problem, there is a better solution.

The Right Way: Use your accessor

UPDATE 2008-12-02: Don’t do this. See the section I added below.

[self addFramistansObject:newFramistan];

Look at that.

Beautiful, isn’t it?

It’s almost as short as the direct array access, and it does the Right Thing with KVO. Your array controller will find out about the change without you having to tie knowledge of the array controller into your model.

We should also look at the accessor:

- (void) addFramistansObject:(Framistan *)newFramistan {
    [framistans addObject:newFramistan];
}

Also short, and also beautiful. (Yes, that is the complete definition of the method. I’m not eliding anything.) And there are no separation violations here, either: It’s pure model.

When you bind the array controller to your object, KVO wraps your object and all its KVC-compliant accessors, including this method. Its implementation performs the proper KVO notifications around a call to your implementation, which means you don’t have to do any KVO work at all.

UPDATE 2008-11-30: Note that you must also implement removeKeyObject:; if you don’t, addKeyObject: will not post the KVO notifications. You must implement both. Thanks to Dave Dribin, who emailed me about this.

ADDED 2008-12-02: The current Right Way: Use indexed accessors

KVO does not work correctly with addKeyObject: and removeKeyObject: for array properties.

The problem is that it treats those methods as set accessors and posts set-mutation notifications, regardless of the fact that it’s an array property. This doesn’t seem to cause a problem with Bindings (as of 10.5.5), but when I tried observing the property using KVO directly, I got a crash every time, as it tried to send NSSet messages to my NSArray.

Instead, you’ll need to use indexed accessors:

[self insertObject:newFramistan inFramistansAtIndex:[self countOfFramistans]];

As with addObject: and removeObject:, you must implement both of the pair. Here’s what removal looks like:

[self removeObjectFromFramistansAtIndex:[self indexOfObjectInFramistans:framistanToRemove]];

indexOfObjectInKey isn’t actually something KVC looks for, but it fits the use case and beats getting the entire array (which generally means copying it) just to find the index of one object. And you have to implement all these methods anyway.

Note that you should not implement addKeyObject: and removeKeyObject: at all for an array property, even to call the indexed methods. That’s because KVO always posts its erroneous set-mutation notifications around your addKeyObject: and removeKeyObject: methods, regardless of how you implemented them.

Thanks again to Dave Dribin, as most of this came up in the same email thread.

Further reading

Leave a Reply

Do not delete the second sentence.