Observing an NSMutableArray for insertion/removal

35,304

Solution 1

But shouldn't the synthesized accessors automatically return such a proxy object?

No.

What's the proper way to work around this--should I write a custom accessor that just invokes [super mutableArrayValueForKey...]?

No. Implement the array accessors. When you call these, KVO will post the appropriate notifications automatically. So all you have to do is:

[myObject insertObject:newObject inTheArrayAtIndex:[myObject countOfTheArray]];

and the Right Thing will happen automatically.

For convenience, you can write an addTheArrayObject: accessor. This accessor would call one of the real array accessors described above:

- (void) addTheArrayObject:(NSObject *) newObject {
    [self insertObject:newObject inTheArrayAtIndex:[self countOfTheArray]];
}

(You can and should fill in the proper class for the objects in the array, in place of NSObject.)

Then, instead of [myObject insertObject:…], you write [myObject addTheArrayObject:newObject].

Sadly, add<Key>Object: and its counterpart remove<Key>Object: are, last I checked, only recognized by KVO for set (as in NSSet) properties, not array properties, so you don't get free KVO notifications with them unless you implement them on top of accessors it does recognize. I filed a bug about this: x-radar://problem/6407437

I have a list of all the accessor selector formats on my blog.

Solution 2

I would not use willChangeValueForKey and didChangeValueForKey in this situation. For one, they're meant to indicate the value at that path has changed, not that values in a to-many relationship are changing. You would want to use willChange:valuesAtIndexes:forKey: instead, if you did it this way. Even so, using manual KVO notifications like this is bad encapsulation. A better way of doing it is defining a method addSomeObject: in the class that actually owns the array, which would include the manual KVO notifications. This way, outside methods that are adding objects to the array don't need to worry about handling the array owner's KVO as well, which wouldn't be very intuitive and could lead to unneeded code and possibly bugs if you start adding objects to the array from several places.

In this example I would actually continue to use mutableArrayValueForKey:. I'm not positive with mutable arrays, but I believe from reading the documentation that this method actually replaces the entire array with a new object, so if performance is a concern you'll also want to implement insertObject:in<Key>AtIndex: and removeObjectFrom<Key>AtIndex: in the class that owns the array.

Solution 3

when you just want to observe the count changed, you may use an aggregate key path:

[myObj addObserver:self forKeyPath:@"theArray.@count" options:0 context:NULL];

but be aware that any reordering in theArray will not fire.

Solution 4

Your own answer to your own question is almost right. Don't vend theArray externally. Instead, declare a different property, theMutableArray, corresponding to no instance variable, and write this accessor:

- (NSMutableArray*) theMutableArray {
    return [self mutableArrayValueForKey:@"theArray"];
}

The result is that other objects can use thisObject.theMutableArray to make changes to the array, and these changes trigger KVO.

The other answers pointing out that efficiency is increased if you also implement insertObject:inTheArrayAtIndex: and removeObjectFromTheArrayAtIndex: are still correct. But there is no need for other objects to have to know about these or call them directly.

Solution 5

If you don't need the setter you can also use the simpler form below, which has similar performance (same growth rate in my tests) and less boilerplate.

// Interface
@property (nonatomic, strong, readonly) NSMutableArray *items;

// Implementation
@synthesize items = _items;

- (NSMutableArray *)items
{
    return [self mutableArrayValueForKey:@"items"];
}

// Somewhere else
[myObject.items insertObject:@"test"]; // Will result in KVO notifications for key "items"

This works because if the array accessors are not implemented and there is no setter for the key, mutableArrayValueForKey: will look for an instance variable with the name _<key> or <key>. If it finds one, the proxy will forward all messages to this object.

See these Apple docs, section "Accessor Search Pattern for Ordered Collections", #3.

Share:
35,304

Related videos on Youtube

Adam Ernst
Author by

Adam Ernst

I'm an iOS developer based in New York City. I made one of the first 500 apps on the App Store with iTrans, my NYC subway app. I did the Khan Academy iPad app and the Turntable.fm iPhone app. Now I blog and work at Facebook on their iOS app.

Updated on July 05, 2022

Comments

  • Adam Ernst
    Adam Ernst almost 2 years

    A class has a property (and instance var) of type NSMutableArray with synthesized accessors (via @property). If you observe this array using:

    [myObj addObserver:self forKeyPath:@"theArray" options:0 context:NULL];
    

    And then insert an object in the array like this:

    [myObj.theArray addObject:NSString.string];
    

    An observeValueForKeyPath... notification is not sent. However, the following does send the proper notification:

    [[myObj mutableArrayValueForKey:@"theArray"] addObject:NSString.string];
    

    This is because mutableArrayValueForKey returns a proxy object that takes care of notifying observers.

    But shouldn't the synthesized accessors automatically return such a proxy object? What's the proper way to work around this--should I write a custom accessor that just invokes [super mutableArrayValueForKey...]?

  • Chris Hanson
    Chris Hanson over 15 years
    The #1 issue is that when you add an observer, you are observing a property of some object. The array is the value of that property, not the property itself. That's why you need to either use the accessors or -mutableArrayValueForKey: to modify the array.
  • Michael Mior
    Michael Mior about 12 years
    When using this shortcut, I am only able to observe changes on key path "theArray", not "theMutableArray."
  • Michael Mior
    Michael Mior about 12 years
    Other than that, everything functions perfectly and I can manipulate theMutableArray just as if it were an NSMutableArray property.
  • Luong Huy Duc
    Luong Huy Duc almost 11 years
    When I try to lazy init this proxy object, no KVO notification is emitted. Do you know why? - (NSMutableArray*) theMutableArray { if (_theMutableArray) return _theMutableArray; _theMutableArray = [self mutableArrayValueForKey:@"theArray"]; return _theMutableArray; }
  • Patrick Pijnappel
    Patrick Pijnappel about 10 years
    Or replacements for that matter, e.g. when [-replaceObjectAtIndex:withObject:] is used.
  • Entalpi
    Entalpi about 10 years
    For some reason this does not work for me. I might be real blind in this case though.
  • Bryan
    Bryan over 9 years
    Your last point seems to be out of date - I get free KVO notifications on NSArray properties, provided I implement both add and remove accessors.
  • Symaxion
    Symaxion over 7 years
    When using this solution, make sure to mark your properties as readonly, or else this will be stuck in an infinite loop...