10

I am trying to use KVO to listen to collection change events on an NSArray property. Publicly, the property is a readonly NSArray, but is backed by an NSMutableArray ivar so that I can modify the collection.

I know I can set the property to a new value to get a “set” change, but I’m interested in add, remove, replace changes. How do I correctly notify these type of changes for an NSArray?

@interface Model : NSObject

@property (nonatomic, readonly) NSArray *items;

@end

@implementation Model {
    NSMutableArray *_items;
}

- (NSArray *)items {
    return [_items copy];
}

- (void)addItem:(Item *)item {
  [_items addObject:item];
}

@end

Model *model = [[Model alloc] init];

[observer addObserver:model 
      forKeyPath:@"items" 
         options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) 
         context:NULL];

Item *item = [[Item alloc] init];
[model addItem:newItem];

Observer class:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"items"]) {
        //Not called
    }
}
Kevin DiTraglia
  • 25,746
  • 19
  • 92
  • 138
  • Can you post what code you've written so far? – Holly Jun 06 '14 at 19:08
  • Also, you should probably review [this](http://stackoverflow.com/questions/3478451/key-value-observing-with-an-nsarray) post. – Holly Jun 06 '14 at 19:10
  • The duplicate question seems to put the mutable array in the header, is there anyway to accomplish this without giving public access to mutate the array in question? – Kevin DiTraglia Jun 06 '14 at 19:15
  • The only reason the ivar in the other question is in the header is that the code predates `@implementation` ivars. The core of the solution is the indexed accessor methods. Note Peter Hosey's answer -- you should _not_ expose the array. – jscs Jun 06 '14 at 19:21
  • @JoshCaswell The heart of my question is how to observe changes to a public NSArray that is backed by a private NSMutableArray. I read the duplicate question prior to asking this question, but it did not answer my problem of how to wire up KVO to observe changes to an array without exposing the internal mutable array. – Kevin DiTraglia Jun 06 '14 at 19:25
  • Also every answer seems to allude to using `NSArrayController` which is not available in iOS. – Kevin DiTraglia Jun 06 '14 at 19:26

1 Answers1

20

First, you should understand that KVO is for observing an object for changes in its properties. That is, you can't "observe an array" as such, you observe an indexed collection property. That property may be backed by an array or implemented in some other way. So long as it is compliant with KVC and modified in a KVO-compliant manner, that's enough. (So, it doesn't matter if the property is of type NSArray* or implemented using an NSMutableArray* or anything.)

So, you're observing an instance of Model for changes in its items property. If you want the observer to get a change notification, you have to be sure to always modify the items property in a KVO-compliant manner.

The best way, in my opinion, is to implement the mutable indexed collection accessors and always use those to modify the property. So, you'd implement at least one of these:

- (void) insertObject:(id)anObject inItemsAtIndex:(NSUInteger)index;
- (void) insertItems:(NSArray *)objects atIndexes:(NSIndexSet *)indexes;

And one of these:

- (void) removeObjectFromItemsAtIndex:(NSUInteger)index;
- (void) removeItemsAtIndexes:(NSIndexSet *)indexes;

When the property is backed by an NSMutableArray, the above methods are straightforward wrappers around the corresponding methods on _items.

Any other methods you write to modify your property should go through one of those. So, your -addItem: method would be:

- (void)addItem:(Item *)item {
    [self insertObject:item inItemsAtIndex:[_items count]];
}

You could also remove the plain getter for the items property and instead only expose the indexed collection getters:

- (NSUInteger) countOfItems;
- (id) objectInItemsAtIndex:(NSUInteger)index;

That's not necessary, though, if there is a typical getter.

(The existence of those accessors is what allows you to implement a to-many property that is not of type NSArray. There's no need, from KVC's point of view, for any actual array-typed interface.)

Personally, I don't recommend this, but once you have such accessors, you can also mutate the property by obtaining the NSMutableArray-like proxy for the property using -mutableArrayValueForKey: and then sending mutation operations to it. So, in this case, you might do [[self mutableArrayValueForKey:@"items"] addObject:item]. I don't like this because I feel that key-value coding is for when the key is data. It's dynamic or stored in a data file like a NIB, not known at compile time. Hard-coding key names when you have the option to use a language symbol (e.g. selector) to address the property is a code smell.

It can be justified, though, for operations that are truly tortuous to implement in terms of the indexed accessors, such as sorting.

Finally, you can use the NSKeyValueObserving protocol's -willChange... and -didChange... methods to emit change notifications when you directly modify the property's backing storage without going through a mutation method that KVO can recognize and hook into. For an indexed collection property, that would be the -willChange:valuesAtIndexes:forKey: and -didChange:valuesAtIndexes:forKey: methods. This is an even worse code smell, as far as I'm concerned.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154