views:

888

answers:

3

I have an object that implements the indexed accessor methods for a key called contents. In those accessors, I call willChange:valuesAtIndexes:forKey: and didChange:valuesAtIndexes:forKey: when I modify the underlying array.

I also have a custom view object that is bound to contents via an NSArrayController. In observeValueForKeyPath:ofObject:change:context: the only value in the change dictionary for the NSKeyValueChangeKindKey I ever see is NSKeyValueChangeSetting. When I'm adding objects to the array, I expect to see NSKeyValueChangeInsertion.

Recreating my view's internal representation of the objects it observes every time I insert a single item -- particularly when I'm bulk loading hundreds of items -- presents quite a performance problem, as you'd imagine. What am I doing wrong that Cocoa seems to think I'm setting a completely new array each time I add or remove a single item?

A: 

I have an object that implements the indexed accessor methods for a key called contents. In those accessors, I call willChange:valuesAtIndexes:forKey: and didChange:valuesAtIndexes:forKey: when I modify the underlying array.

Don't do that. KVO posts the notifications for you when you receive a message to one of those accessors.

I also have a custom view object that is bound to contents via an NSArrayController. In observeValueForKeyPath:ofObject:change:context: the only value in the change dictionary for the NSKeyValueChangeKindKey I ever see is NSKeyValueChangeSetting. When I'm adding objects to the array, I expect to see NSKeyValueChangeInsertion.

For one thing, why are you using KVO directly? Use bind:toObject:withKeyPath:options: to bind the view's property to the array controller's arrangedObjects (I assume) property, and implement array accessors (including indexed accessors, if you like) in the view.

For another, remember that arrangedObjects is a derived property. The array controller will filter and sort its content array; the result is arrangedObjects. You could argue that permuting the indexes from the original insertion into a new insertion would be a more accurate translation of the first change into the second, but setting the entire arrangedObjects array was probably simpler to implement (something like [self _setArrangedObjects:[[newArray filteredArrayUsingPredicate:self.filterPredicate] sortedArrayUsingDescriptors:self.sortDescriptors]]).

Does it really matter? Have you profiled and found that your app is slow with wholesale array replacement?

If so, you may need to bind the view directly to the array's content property or to the original array on the underlying object, and suffer the loss of free filtering and sorting.

Peter Hosey
A: 

I call the KVO methods manually for reasons outside the scope of this issue. I have disabled automatic observing for this property. I know what I'm doing there.

I don't understand what you mean by implementing array accessors in the view. Bindings is built on top of KVO. All bindings are implemented using observeValueForKeyPath: My custom view provides a binding that the app binds to an array -- or in this case, an array controller. Accessor methods apply to KVC, not KVO.

It absolutely matters that I'm getting a set message on every array update. I wouldn't have posted it as a question if it didn't matter. I call something like

[[modelObject mutableArrayValueForKey:@"contents"] addObjectsFromArray:hundredsOfObjects];

On every insertion, my view observes a whole new array. Since I'm potentially adding hundreds of objects, that's O(N^2) when it should to be O(N). That is completely unacceptable, performance issues aside. But, since you mention it, the view does have to do a fair amount of work to observe a whole new array, which significantly slows down the program.

I can't give up using an array controller because I need the filtering and sorting, and because there's an NSTableView bound to the same controller. I rely on it to keep the sorting and selections in sync.

I solved my problem with a complete hack. I wrote a separate method that calls the KVO methods manually so that only one KVO message is sent. It's a hack, I don't like it, and it still makes my view reread the entire array -- although only once, now -- but it works for now until I figure out a better solution.

Alex
+1  A: 

(Note to all readers: I hate using answers for this, too, but this discussion is too long for comments. The downside, of course, is that it ends up not sorted chronologically. If you don't like it, I suggest you complain to the Stack Overflow admins about comments being length-limited and plain-text-only.)

I don't understand what you mean by implementing array accessors in the view.

Implement accessors, including indexed accessors, for the mutable array property that you've exposed as a binding.

Bindings is built on top of KVO.

And KVC.

All bindings are implemented using observeValueForKeyPath:

Overriding that is one way, sure. The other way is to implement accessors in the object with the bindable property (the view).

My custom view provides a binding that the app binds to an array -- or in this case, an array controller. Accessor methods apply to KVC, not KVO.

Cocoa Bindings will call your view's accessors for you (presumably using KVC). You don't need to implement the KVO observe method (unless, of course, you're using KVO directly).

I know this because I've done it that way. See PRHGradientView in CPU Usage.

Curiously, the documentation doesn't mention this. I'm going to file a documentation bug about it—either I'm doing something fragile or they forgot to mention this very nice feature in the docs.

It absolutely matters that I'm getting a set message on every array update. I wouldn't have posted it as a question if it didn't matter.

There are quite a large number of people who engage in something called “premature optimization”. I have no way of knowing who is one of them without asking.

Peter Hosey
Yes, it's not documented at all. I've never seen this before, including in any of the examples I've seen. Has this been possible since Panther?
Alex
Yeah, it's always worked for me. I've filed a documentation bug: x-radar://problem/6588723 (http://openradar.appspot.com/6588723).
Peter Hosey