views:

117

answers:

3

Background: I use a ton of NSDictionary objects in my iPhone and iPad code. I'm sick of the verbose way of getting/setting keys to these state dictionaries.

So a little bit of an experiment: I just created a class I call Remap.

Remap will take any arbitrary set[VariableName]:(NSObject *) obj selector and forward that message to a function that will insert obj into an internal NSMutableDictionary under the key [vairableName].

Remap will also take any (zero argument) arbitrary [variableName] selector and return the NSObject mapped in the NSMutableDictionary under the key [variableName].

e.g.

Remap * remap = [[Remap alloc] init];

NSNumber * testNumber = [NSNumber numberWithInt:46];
[remap setTestNumber:testNumber];
testNumber = [remap testNumber];

[remap setTestString:@"test string"];
NSString * testString = [remap testString];

NSMutableDictionary * testDict = [NSMutableDictionary dictionaryWithObject:testNumber forKey:@"testNumber"];
[remap setTestDict:testDict];
testDict = [remap testDict];

where none of the properties testNumber, testString, or testDict are actually defined in Remap.

The crazy thing? It works... My only question is how can I disable the "may not respond to " warnings for JUST accesses to Remap?

P.S. : I'll probably end up scrapping this and going with macros since message forwarding is quite inefficient... but aside from that does anyone see other problems with Remap?

Here's Remap's .m for those who are curious:

#import "Remap.h"

@interface Remap ()
@property (nonatomic, retain)   NSMutableDictionary * _data;
@end

@implementation Remap

@synthesize _data;

- (void) dealloc
{
    relnil(_data);
    [super dealloc];
}

- (id) init
{
    self = [super init];
    if (self != nil) {
        NSMutableDictionary * dict = [[NSMutableDictionary alloc] init];
        [self set_data:dict];
        relnil(dict);
    }
    return self;
}


- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSString * selectorName = [NSString stringWithUTF8String: sel_getName([anInvocation selector])];

    NSRange range = [selectorName rangeOfString:@"set"];

    NSInteger numArguments = [[anInvocation methodSignature] numberOfArguments];

    if (range.location == 0 && numArguments == 4)
    {
        //setter
        [anInvocation setSelector:@selector(setData:withKey:)];
        [anInvocation setArgument:&selectorName atIndex:3];
        [anInvocation invokeWithTarget:self];
    }
    else if (numArguments == 3)
    {
        [anInvocation setSelector:@selector(getDataWithKey:)];
        [anInvocation setArgument:&selectorName atIndex:2];
        [anInvocation invokeWithTarget:self];
    }


}

- (NSMethodSignature *) methodSignatureForSelector:(SEL) aSelector
{
    NSString * selectorName = [NSString stringWithUTF8String: sel_getName(aSelector)];

    NSMethodSignature * sig = [super methodSignatureForSelector:aSelector];

    if (sig == nil)
    {
        NSRange range = [selectorName rangeOfString:@"set"];

        if (range.location == 0)
        {
            sig = [self methodSignatureForSelector:@selector(setData:withKey:)];
        }
        else
        {
            sig = [self methodSignatureForSelector:@selector(getDataWithKey:)];
        }
    }

    return sig;
}

- (NSObject *) getDataWithKey: (NSString *) key
{
    NSObject * returnValue = [[self _data] objectForKey:key];
    return returnValue;
}


- (void) setData: (NSObject *) data withKey:(NSString *)key
{
    if (key && [key length] >= 5 && data)
    {
        NSRange range;
        range.length = 1;
        range.location = 3;

        NSString * firstChar = [key substringWithRange:range];
        firstChar = [firstChar lowercaseString];

        range.length = [key length] - 5; // the 4 we have processed plus the training :
        range.location = 4;

        NSString * adjustedKey = [NSString stringWithFormat:@"%@%@", firstChar, [key substringWithRange:range]];

        [[self _data] setObject:data forKey:adjustedKey];
    }
    else 
    {
        //assert?
    }
}

@end
A: 

As an alternative for making dictionaries more like general purpose objects, what I have done is make categories of NSDictionary that wrap getters and setters for specific keys. Just use very descriptive names since you are filling a global namespace.

@interface NSDictionary (MyGetters)
@property (nonatomic,readonly) id something;
-(id) something;
@end

@interface NSDictionary (MyGetters)
-(id) something { return [self objectForKey:@"something"]; }
@end


@interface NSMutableDictionary (MySetters)
-(void) setSomething:(id)inValue;
@end

@interface NSDictionary (MySetters)
-(void) setSomething:(id)inValue { return [self setObject:inValue forKey:@"something"]; }
@end

The only problem with setter properties is that you must define the getter in both categories to avoid a warning.

You still have to declare everything, but you are going to have to do that anyway to get rid of warnings for Remap.

drawnonward
Yes, I have considered this too. But really I'm trying to find the ultimate solution to this to avoid having to write code every time I want a new value in my dictionary.
awolf
The ultimate solution is to change the way the compiler works by adding a language supported object. While you are at it, can you add constant NSArrays: @["a","b","c"]; cause I would really like that feature too.
drawnonward
+1  A: 

If I'm reading your code right, I think a potential problem with this approach might be that you can't have key names such as hash (or other methods from NSObject, assuming your Remap inherits from NSObject). You will end up with the Remap instance's hash value rather than letting Remap look up a key called hash within _data, because [remap hash] will not invoke forwardIncovation:, as far as I can tell.

dreamlax
The work around to that would be calling [super respondsToSelector:] and only treating unsupported selectors as keys.
drawnonward
@dragonward actually the NSObject's hash would still get called since the [super methodSignatureForSelector:aSelector] (where aSelector == @selector(hash)) would find an existing function and not enter my code.As a brutre force work-around I could override each of NSObject's properties and make sure it called my functions instead.(Of course... this would be a problem if I really DID want to call NSObject's hash... but I can live with this limitation).
awolf
+2  A: 

Cool class. I like it.

I can't think of a way to suppress all your warnings, but we can get it down to one line per property. Add this to your Remap.h:

#define RemapProperty(PROP) \
@interface Remap(PROP) \
@property (nonatomic, retain) id PROP; \
@end \
@implementation Remap(PROP) \
@dynamic PROP; \
@end

Now you can suppress all remap warnings for a given property by putting this at the top of the file that's giving you a warning:

RemapProperty(propertyName);

This requires some extra work, but it gives you dot-syntax as a reward.

remap.propertyName = @"Foo";

With a little more work, you could define a similar macro that adds properties directly to NSDictionary, thereby making the Remap class unnecessary.

cduhn
Thanks, good idea. This make Remap very nearly usable... Perhaps I'll do some benchmarks to see hoe much of a performance hit I would be taking.
awolf