Search code examples
iosobjective-cios7nsarraykey-value-observing

KVO Notifications for a Modification of an NSArray backed by a NSMutableArray


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
    }
}

Solution

  • 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.