views:

4055

answers:

4

I'm trying to work out the "correct" way to handle populating an array with key-value coding for an iPhone app. I've come up with something that works, but it's fairly hackish. Basically I'm parsing an XML document into a set of code-generated models. Let's assume the XML is of this format:

<foo>
    <bar>
        <item name="baz" />
        <item name="bog" />
    </bar>
</foo>

On my generated object that represents the Bar element, I have an NSMutableArray defined for the sub-node:

@interface Bar : NSObject {
    NSMutableArray *item;
}
@end

So by default when I call setValue:forKey: on an instance of Bar, it ends up overwriting the instance of NSMutableArray with a single instance of the Item object. What I've currently done to get this working is where it gets hacky. I renamed the array instance variable to be something else, let's say the plural form of the name:

@interface Bar : NSObject {
    NSMutableArray *items;
}
@end

This causes the default accessor for setValue:forKey: to miss. I then added this method to the Bar implementation:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if([key isEqualToString:@"item"]) {
        if(items == nil) {
            items = [[NSMutableArray alloc] init];
            [items retain];   
        }
        [items addObject:value];
    }
}

And everything works! I'm sure there must be a better way to do this though! I've read through the Key-Value Coding Programming Guide, but I must be missing something, because I'm unclear as to how things are supposed to work for array accessors. I've tried implementing countOf: and objectInAtIndex: as they KVC Programming guide seems to indicate, but that still results with my NSMutableArray being overwritten with an instance of the Item type.

A: 

Well, there's 2 ways you can go about it:

  1. Add an "Add Item" method to your Bar class where you pass in the item
  2. Make sure you're passing in the whole array of items when doing KVC

I don't believe there is a way to add an item to an array using KVC.

Martin Pilkington
That's true, there is no way to observe the addition and removal of items from an array directly. You have to implement the key-value observing calls yourself from your add and remove methods.
Jason Coco
So all of the talk about to-many support in the KVC docs is just there to throw me off? ;)
defeated
No, there is support for to-many, you just can't observe a basic array to see additions and deletions...
Jason Coco
+1  A: 

As Martin suggests, you cannot directly observe an array to see when items are added or removed. Instead, this would be considered a "to-many" attribute of your Bar object, and you would have to deal with making the KVO assertions yourself. You can do something like this for adding and removing items in bar:

@interface Bar : NSObject
{
    NSMutableArray *items;
}
-(void)addItem:(id)item;
-(void)removeItem:(id)item;
@end

@implementation Bar
-(id)init
{
    if( (self = [super init]) ) {
     items = [[NSMutableArray alloc] init];
    }
    return self;
}

-(void)addItem:(id)item
{
    NSParameterAssert(item);
    NSIndexSet *iset = [NSIndexSet indexSetWithIndex:[items count]];
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:iset forKey:@"items"];
    [items addObject:item];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:iset forKey:@"items"];
}

-(void)removeItem:(id)item
{
    NSParameterAssert(item);
    NSIndexSet *iset = [NSIndexSet indexSetWithIndex:[items indexForObject:item]];
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:iset forKey:@"items"];
    [items removeObject:item];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:iset forKey:@"items"];
}
@end

If you wanted to be able to set/replace items directly, you would have to come up with some way to do that yourself. Normally, I would suggest and NSArrayController for this kind of thing, but since you're using the iPhone, you will have to basically create this functionality yourself.

Jason Coco
I'm a bit new to objective-c, so forgive me if this is obvious, but I still wouldn't be able to call: [bar setValue:@"baz" forKey:@"item"] right? The problem is this is part of parsing an xml doc, so I'm trying to maintain the ability to set arbitrary properties on the receiving object.
defeated
No, you wouldn't, but since you know that your DTD says that multiple items are allowed, instead of calling setValue you would call addItem. Your other option might be to use a dictionary instead and intercept set Value. I will add an example of that as well...
Jason Coco
+2  A: 

If you want to use KVC properties for an array you should look into the documentation for mutableArrayValueForKey:

Basically with your class:

@interface Bar : NSObject {
    NSMutableArray *items;
}
@end

You would define these methods as a minimum, assuming you initialized your array elsewhere:

- (void)insertObject:(id)object inItemsAtIndex:(NSUInteger)index {
    [items insertObject:object atIndex:index];
}

- (void)removeObjectFromItemsAtIndex:(NSUInteger)index {
    [items removeObjectAtIndex:index];
}

- (NSArray *)items {
    /* depending on how pedantic you want to be
       possibly return [NSArray arrayWithArray:items] */
    return items;
}

Once you've implemented those methods you can call [bar mutableArrayValueForKey:@"items"]. It will return an NSMutableArray proxy object that you can add and remove objects from. That object will in turn generate the appropriate KVO messages and uses the methods you just defined to interact with your actual array.

Ashley Clark
Thanks, this is the closest to what I'm trying to achieve. I had all of the pieces of this in place, but apparently not at the same time ;).
defeated
A: 

It is sufficient to call valueForKey:@"item" and the result will be mutable. So you can say:

NSMutableArray* m = [theBar valueForKey:@"item"];
[item addObject: value];

You don't need those other methods unless you're implementing a facade (a key for which there is not mutable instance variable).

matt