Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing my own @dynamic properties in Cocoa

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!)

like image 824
Amy Worrall Avatar asked Aug 24 '10 19:08

Amy Worrall


Video Answer


2 Answers

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.

like image 145
Amy Worrall Avatar answered Oct 22 '22 04:10

Amy Worrall


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...

like image 42
Max Seelemann Avatar answered Oct 22 '22 02:10

Max Seelemann