views:

1586

answers:

3

I am trying to get key-value-observing to work for an NSMutableArray. Below is the .h file for MyObservee, the observed class:

@interface MyObservee : NSObject {
    @private int someValue;
    @private NSMutableArray *someArray;
}

@property (readwrite,assign) int someValue;
- (NSMutableArray *)someArray;
@end

The class MyObserver implements observeValueForKeyPath:ofObject:change:context:. Here is how I add the observer:

MyObservee *moe = [[MyObservee alloc] init];
MyObserver *mobs = [[MyObserver alloc] init];

[moe addObserver:mobs 
   forKeyPath:@"someArray" 
   options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) 
   context:NULL];

[[moe someArray] addObject:@"hi there"];

How come the addObject: message isn't triggering as a change to the someArray key path? I have a feeling there's something I don't fully understand here.

+2  A: 

Unfortunately, The NSArray classes are note KVO compliant. They are KVC compliant, but you can't observe them directly like you're trying to do here. The easiest way to get this functionality would be to use an NSArrayController. The NSArray controller is KVO compliant and will alert you when items are added or removed. In your example, your observer would be notified if you actually changed the array itself. For instance, if you did something like this:

[moe setSomeArray:[NSMutableArray array]];

Which is probably not what you wanted at all :) Just as an aside, NSDictionary is actually KVO compliant so you could use that, if you chose. Or you could write a wrapper subclass of NSMutableArray that just creates a real mutable array as its backing store but just forwards on all messages to it except addObject and removeObject which you could override to trigger notifications.

Jason Coco
+5  A: 

You need to implement the indexed array accessors as defined in the KVC programming guide. Then you must use those accessors to access the array and the KVO triggering will work. You can also call -mutableArrayValueForKey: and use that array to addObject: and such and it will in turn call the accessor methods and the KVO triggering will occur as well. There are also set accessors for use in for NSSets, see here and here.

Example:

@interface MyClass : NSObject
{
    NSMutableArray *_orders;
}

@property(retain) NSMutableArray *orders;

- (NSUInteger)countOfOrders;
- (id)objectInOrdersAtIndex:(NSUInteger)index;
- (void)insertObject:(id)obj inOrdersAtIndex:(NSUInteger)index;
- (void)removeObjectFromOrdersAtIndex:(NSUInteger)index;
- (void)replaceObjectInOrdersAtIndex:(NSUInteger)index withObject:(id)obj;


@end
robottobor
or... you could just use an NSArrayController ;-)
Jason Coco
Jason Coco: Yes, one generally should use an NSArrayController, but that doesn't solve the problem of how to mutate the underlying model and get KVO notifications.
Peter Hosey
+2  A: 

Why are you passing your private array to another object? It's not so private when you let other objects handle it.

As s-bug said, you should implement the accessors and use mutableArrayValueForKey: to mutate the property. I add that you should not be exposing that private array at all—your someArray method should return an immutable copy of the array.

Furthermore, I call your attention to Jason Coco's comment on s-bug's answer. To paraphrase him, you should probably use an NSArrayController as an additional step of separation between myObservee and myObserver. This is a very good suggestion, and if you don't have a specific reason to directly observe the property, you should take it. (Among the benefits is that you can then use Bindings to connect views to the new array controller.)

Peter Hosey