16

Suppose (for the sake of argument) that I have a view class which contains an NSDictionary. I want a whole bunch of properties, all of which access the members of that dictionary.

For example, I want @property NSString* title and @property NSString* author.

For each one of these properties, the implementation is the same: for the getter, call [dictionary objectForKey:propertyName];, and for the setter do the same with setObject:forKey:.

It would take loads of time and use loads of copy-and-paste code to write all those methods. Is there a way to generate them all automatically, like Core Data does with @dynamic properties for NSManagedObject subclasses? To be clear, I only want this means of access for properties I define in the header, not just any arbitrary key.

I've come across valueForUndefinedKey: as part of key value coding, which could handle the getters, but I'm not entirely sure whether this is the best way to go.

I need these to be explicit properties so I can bind to them in Interface Builder: I eventually plan to write an IB palette for this view.

(BTW, I know my example of using an NSDictionary to store these is a bit contrived. I'm actually writing a subclass of WebView and the properties will refer to the IDs of HTML elements, but that's not important for the logic of my question!)

Amy Worrall
  • 16,250
  • 3
  • 42
  • 65

5 Answers5

18

I managed to solve this myself after pouring over the objective-c runtime documentation.

I implemented this class method:

+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
    NSString *method = NSStringFromSelector(aSEL);

    if ([method hasPrefix:@"set"])
    {
        class_addMethod([self class], aSEL, (IMP) accessorSetter, "v@:@");
        return YES;
    }
    else
    {
        class_addMethod([self class], aSEL, (IMP) accessorGetter, "@@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

Followed by a pair of C functions:

NSString* accessorGetter(id self, SEL _cmd)
{
    NSString *method = NSStringFromSelector(_cmd);
    // Return the value of whatever key based on the method name
}

void accessorSetter(id self, SEL _cmd, NSString* newValue)
{
    NSString *method = NSStringFromSelector(_cmd);

    // remove set
    NSString *anID = [[[method stringByReplacingCharactersInRange:NSMakeRange(0, 3) withString:@""] lowercaseString] stringByReplacingOccurrencesOfString:@":" withString:@""];

    // Set value of the key anID to newValue
}

Since this code tries to implement any method that is called on the class and not already implemented, it'll cause problems if someone tries calling something you're note expecting. I plan to add some sanity checking, to make sure the names match up with what I'm expecting.

Amy Worrall
  • 16,250
  • 3
  • 42
  • 65
  • 2
    Only tweak I would make is to only lowercase the first letter (and making sure the _cmd string has at least 5 characters): NSString *property = NSStringFromSelector(_cmd); NSString *firstLetter = [[property substringWithRange:NSMakeRange(3, 1)] lowercaseString]; property = [firstLetter stringByAppendingString:[property substringWithRange:NSMakeRange(4, [property length] - 5)]]; – charles Dec 04 '12 at 09:57
  • Great example. I'd gotten as far as a kind of frozen IMP (i.e. for `setName:` my IMP was `setValueForName`) but your use of `NSStringFromSelector(_cmd)` gave me exactly the kick in the mental pants I was looking for to generalize this to `setWhatever:`. Thanks for writing it up. – matt Dec 11 '12 at 16:34
2

You can use a mix of your suggested options:

  • use the @dynamic keyword
  • overwrite valueForKey: and setValue:forKey: to access the dictionary
  • use the objective-c reflection API's method class_getProperty and check it for nil. If it's not nil your class has such a property. It doesn't if it is.
  • then call the super method in the cases where no such property exists.

I hope this helps. Might seem a bit hacky (using reflection) but actually this is a very flexible and also absolutely "legal" solution to the problem...

PS: the coredata way is possible but would be total overkill in your case...

Max Seelemann
  • 9,344
  • 4
  • 34
  • 40
2

Befriend a Macro? This may not be 100% correct.

#define propertyForKey(key, type) \
    - (void) set##key: (type) key; \
    - (type) key;

#define synthesizeForKey(key, type) \
    - (void) set##key: (type) key \
    { \
         [dictionary setObject];// or whatever \
    } \
    - (type) key { return [dictionary objectForKey: key]; }
Stephen Furlani
  • 6,794
  • 4
  • 31
  • 60
0

There is a nice blog with example code with more robust checks on dynamic properties at https://tobias-kraentzer.de/2013/05/15/dynamic-properties-in-objective-c/ also a very nice SO answer at Objective-C dynamic properties at runtime?.

Couple of points on the answer. Probably want to declare an @property in the interface to allow typeahead also to declare the properties as dynamic in the implementation.

Community
  • 1
  • 1
SilentNot
  • 1,661
  • 1
  • 16
  • 25
0

sounds like you should should be using a class instead of a dictionary. you're getting close to implementing by hand what the language is trying to give you.

mariachi
  • 61
  • 2
  • 1
    Yeah, the dictionary thing was a contrived example. I'm actually using this to make a view for an HTML document that you can connect to with bindings. You're right that if that's all I was doing, it'd be a bad plan :) – Amy Worrall Aug 24 '10 at 20:31