views:

21

answers:

2

I'm writing a game using cocos2d-iphone and our stages are defined in .plist files. However, the files are growing large - so I've developed an editor that adds some structure to the process and breaks most of the plist down into fixed fields. However, some elements still require plist editor type functionality, so I have implemented an NSOutlineView on the panels that show 'other parameters'. I am attempting to duplicate the functionality from XCode's 'Property List Editor'.

I've implemented the following system; http://www.stupendous.net/archives/2009/01/11/nsoutlineview-example/

This is very close to what I need, but there is a problem that I've spent most of today attempting to solve. Values for the key column are calculated 'backwards' from the selected item by finding the parent dictionary and using;

return [[parentObject allKeysForObject:item] objectAtIndex:0];

However, when there are multiple items with the same value within a given dictionary in the tree, this statement always returns the first item that has that value (it appears to compare the strings using isEqualToString: or hash values). This leads to the key column showing 'item1, item1, item1' instead of item1, item2, item3 (where items 1-3 all have value ''). I next tried;

-(NSString*)keyFromDictionary:(NSDictionary*)dict forItem:(id)item
{
for( uint i = 0; i < [[dict allKeys] count]; i++ ) {
    id object = [dict objectForKey:[[dict allKeys] objectAtIndex:i]];

    if ( &object == &item ) {
        return [[dict allKeys] objectAtIndex:i];
    }
}   

return nil;
}

But this always returns nil. I was hoping that somebody with a bit more experience with NSOutlineView might be able to provide a better solution. While this problem only appears once in the linked example, I've had to use this a number of times when deleting items from dictionaries for instance. Any help would be greatly appreciated.

+1  A: 
return [[parentObject allKeysForObject:item] objectAtIndex:0];

However, when there are multiple items with the same value within a given dictionary in the tree, this statement always returns the first item that has that value …

Well, yeah. That's what you told it to do: “Get me all the keys for this value; get me the first item in the array; return that”.

… this statement always returns the first item that has that value (it appears to compare the strings using isEqualToString: or hash values).

It's not that statement that's doing it; it's how dictionaries work: Each key can only be in the dictionary once and can only have exactly one object as its value, and this is enforced using the hash of the key and by sending the keys isEqual: messages (not the NSString-specific isEqualToString:—keys are not required to be strings*).

The values, on the other hand, are not uniquified. Any number of keys can have the same value. That's why going from values to keys—and especially to a key—is so problematic.

*Not in NSDictionary, anyway. When you attempt to generate the plist output, it will barf if the dictionary contains any non-string keys.

I next tried;

-(NSString*)keyFromDictionary:(NSDictionary*)dict forItem:(id)item
{
    for( uint i = 0; i < [[dict allKeys] count]; i++ ) {
        id object = [dict objectForKey:[[dict allKeys] objectAtIndex:i]];

        if ( &object == &item ) {
            return [[dict allKeys] objectAtIndex:i];
        }
    }   

    return nil;
}

But this always returns nil.

That's the least of that code's problems.

First, when iterating on an NSArray, you generally should not use indexes unless you absolutely need to. It's much cleaner to use fast enumeration.

Second, when you do need indexes into an NSArray, the correct type is NSUInteger. Don't mix and match types when you can help it.

Third, I don't know what you meant to do with the address-of operator there, but what you actually did was take the address of those two variables. Thus, you compared whether the local variable object is the same variable as the argument variable item. Since they're not the same variable, that test always returns false, which is why you never return an object—the only other exit point returns nil, so that's what always happens.

The problem with this code and the earlier one-liner is that you're attempting to go from a value to a single key, which is contrary to how dictionaries work: Only the keys are unique; any number of keys can have the same value.

You need to use something else as the items. Using the keys as the items would be one way; making a model object to represent each row would be another.

If you go the model-object route, don't forget to prevent multiple rows in the same virtual dictionary from having the same key. An NSMutableSet plus implementing hash and isEqual: would help with that.

You probably should also make the same change to your handling of arrays.

Peter Hosey
Thanks for the code tips - as this is an internal project for internal use only, I've cut corners a bit - but fast enumeration looks wonderful.
John Wordsworth
The main problem I've had is that the NSOutlineView gives you an item in it's delegate functions (not a row number / indexpath). So, figuring out the dictionary key for that value is problematic, but necessary for updates. I thought I could compare the memory address of the item that NSoutlineView passess to it's delegates with the address of the items in the Dictionary - but I guess somewhere one of the items is 'copying' the object so the addresses never match. I think I will have to convert all of the items out of plist format for the outline view to custom objects and back on save...
John Wordsworth
John Wordsworth: NSDictionaries copy their keys. They test keys' equality by (1) comparing their hashes and (2) asking them whether they are equal to each other; you should compare keys the same way.
Peter Hosey