Search code examples
iosobjective-ckey-value-observing

How to implement KVO for readonly derived NSArray property?


I'd like to implement KVO for an NSArray property that is declared as readonly. The getter for this readonly property returns a copy of the private NSMutableArray that backs the backs the public readonly one:

In my .h:

@interface MyClass : NSObject
@property (readonly, nonatomic) NSArray *myArray;
- (void)addObjectToMyArray:(NSObject *)obj;
- (void)removeObjectFromMyArray:(NSObject *)obj;
@end

And in my .m:

@interface MyClass()
@property (strong, nonatomic) NSMutableArray *myPrivateArray;
@end

@implementation MyClass

- (NSArray *)myArray {
    return (NSArray *)[self.myPrivateArray copy];
}

- (void) addObjectToMyArray:(NSObject *)obj {
    [self willChangeValueForKey:@"myArray"];
    [self.myPrivateArray addObject:obj];
    [self didChangeValueForKey:@"myArray"];
}

- (void) removeObjectToMyArray:(NSObject *)obj {
    [self willChangeValueForKey:@"myArray"];
    [self.myPrivateArray removeObject:obj];
    [self didChangeValueForKey:@"myArray"];
}
@end

In my tests, I am seeing an exception thrown when I call didChangeValueForKey:. Is this the correct way to do this?


Solution

  • I recommend that you don't use a separate property for the mutable array. Instead, have the array property backed by a mutable array variable. Then, implement the indexed collection mutating accessors and make all changes to the array through those. KVO knows to hook into those accessors and emit change notifications. In fact, it can emit better, more specific change notifications that can allow observers to be more efficient in how they respond.

    @interface MyClass : NSObject
    @property (readonly, copy, nonatomic) NSArray *myArray;
    - (void)addObjectToMyArray:(NSObject *)obj;
    - (void)removeObjectFromMyArray:(NSObject *)obj;
    @end
    
    @interface MyClass()
    // Optional, if you want to be able to do self.myArray = <whatever> in your implementation
    @property (readwrite, copy, nonatomic) NSArray *myArray;
    @end
    
    @implementation MyClass
    {
        NSMutableArray *_myArray;
    }
    
    @synthesize myArray = _myArray;
    
    // If you optionally re-declared the property read-write internally, above
    - (void) setMyArray:(NSArray*)array {
        if (array != _myArray) {
            _myArray = [array mutableCopy];
        }
    }
    
    - (void) insertObject:(id)anObject inMyArrayAtIndex:(NSUInteger)index {
        [_myArray insertObject:anObject atIndex:index];
    }
    
    - (void) removeObjectFromMyArrayAtIndex:(NSUInteger)index {
        [_myArray removeObjectAtIndex:index];
    }
    
    - (void) addObjectToMyArray:(NSObject *)obj {
        [self insertObject:obj inMyArrayAtIndex:_myArray.count];
    }
    
    - (void) removeObjectToMyArray:(NSObject *)obj {
        NSUInteger index = [_myArray indexOfObject:obj];
        if (index != NSNotFound)
            [self removeObjectFromMyArrayAtIndex:index];
    }
    @end