views:

371

answers:

2

I have a class Test which has an array of Foos. I want to provide access to the Foos without exposing the ivar directly. I'm trying to make this KVC compliant (also to pave the way for KVO compliance). I have:

Test.h

@interface Test : NSObject
{
    NSMutableArray *foos;
}

@property (readonly, copy) NSMutableArray *foos;

@end

Test.m

- (id) init
{
    self = [super init];
    if (self != nil)
    {
        foos = [[NSMutableArray array] retain];
    }
    return self;
}

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

- (NSUInteger)countOfFoos
{
    return [foos count];
}

- (id)objectInFoosAtIndex:(NSUInteger)index
{
    return [foos objectAtIndex:index];
}

- (NSArray *)foosAtIndexes:(NSIndexSet *)indexes
{
    return [foos objectsAtIndexes:indexes];
}

- (void)insertObject:(id)key inFoosAtIndex:(NSUInteger)index
{
    [foos insertObject:key atIndex:index];
}

- (void)insertFoos:(NSArray *)foosArray atIndexes:(NSIndexSet *)indexes
{
    [foos insertObjects:foosArray atIndexes:indexes];
}

- (void)removeObjectFromFoosAtIndex:(NSUInteger)index
{
    [foos removeObjectAtIndex:index];
}

- (void)removeFoosAtIndexes:(NSIndexSet *)indexes
{
    [foos removeObjectsAtIndexes:indexes];
}

This enters an infinite loop when a client tries to add a Foo:

Test *test = [[Test alloc] init];
NSMutableArray *foos = test.foos;
[foos addObject:@"adding object"]; // infinite loop here

What am I doing wrong?

+1  A: 

The problem is that the proxy NSMutableArray returned by mutableArrayValueForKey: first has to get the real array, which it does through the "foos" method. Since that's the one that returns a proxy NSMutableArray it enters an infinite loop. One solution is to use another name:

- (NSMutableArray*) mutableFoos
{
    return [self mutableArrayValueForKey:@"foos"];
}
Luís Marques
+1  A: 
- (NSMutableArray*) foos
{
    return [self mutableArrayValueForKey:@"foos"];
}

An accessor should not use KVC to get the value of the property being accessed; the idea is that KVC goes through the accessors, because the accessors are closer to the value than KVC is.

The correct implementation of foos should return a copy, mutable or otherwise, of the array. Here's how I'd do it:

- (NSArray *) foos
{
    return [[foos copy] autorelease];
}

I would also make all of the accessors public. Anything that wants to mutate the array or randomly access elements at specific indexes can do so that way. It's still safe and encapsulated because they're going through your accessors, not directly accessing the array.

There's not really any reason to use the KVC protocol methods yourself unless you don't know what key you'll access at the time you write the code. For example, if you were writing the nib loader or the Cocoa Bindings system, you would use KVC.

Peter Hosey
Peter, what you say makes sense to me, but as far as I understand if "-foos" returned a copy I wouldn't be able to use KVO on Test.foos. Using the "mutableArrayValueForKey:" in a differently named method (see my own answer) solves that, since the proxy NSMutableArray then goes through the Test KVC-compliant methods. Any comments? Thanks!
Luís Marques
“… if "-foos" returned a copy I wouldn't be able to use KVO on Test.foos.” Yes you would. KVO notifications are posted by (KVO on behalf of) your class, not by your NSMutableArray. `mutableArrayValueForKey:` is a KVO method that creates a proxy array that sends accessor messages to your object, which *in turn* cause KVO notifications; it is not simply your NSMutableArray with a KVO switch flipped. Observers observe your property; they do not observe any particular array value of it. The getter returning a copy does and will work fine, as long as you use your setter and mutator methods.
Peter Hosey
What my code is doing is getting the foos array and manipulating that directly (without the Test's foo accessor methods). What I meant to say is that if Test returned a normal array of foos (either a copy or the original) then KVO wouldn't work, while with the proxy NSMutableArray returned by mutableArrayValueForKey it would, because the proxy calls the accessors. Is using mutableArrayValueForKey wrong? Should I use the Test mutator methods explicitly?
Luís Marques
You should use the mutator accessors directly, because that works and is the cleanest code. The second way would be to call `mutableArrayValueForKey:` yourself, rather than making one of the accessors call it. This way, it is explicit in the call site that you are using a proxy array to do things the KVC way. However, the accessors provide the same benefits without having to go through a proxy, and it's clear enough that you're being KVC-compliant as long as the accessors are named in the right patterns.
Peter Hosey