4
\$\begingroup\$

I'm doing my best to explain how this works, but it's pretty confusing and lengthy. Let me know if there's something I can do to clarify.

I'm building a class that models a custom Plist and sets its values to properties in the class automatically. Most importantly however is the core concept of populating the properties of the subclass with values from the dictionary if they exist. To do this, it is required that your dictionary key be the same as the name of your property, and you are not messing with Apple's autosynthesized setter. Everything is working, but I'm worried that if its implemented incorrectly, it will cause problems. Can someone tell me if there is something I'm missing? First I retrieve an array of our property names, and then I cycle them through this method to set them to dictionary values like this:

[self setPropertyFromDictionaryValueWithName:propertyNames[0]];

And here's what happens:

- (void) setPropertyFromDictionaryValueWithName:(NSString *)propertyName {

    // Get our setter from our string
    SEL propertySetterSelector = [self setterSelectorForPropertyName:propertyName];

    // Make sure it exists as a property
    if ([self respondsToSelector:propertySetterSelector]) {


        if (_realDictionary[propertyName]) {

            // Index 0 is object, Index 1 is the selector: arguments start at Index 2
            const char * typeOfProperty = [self typeOfArgumentForSelector:propertySetterSelector atIndex:2];

            // Get object from our dictionary
            id objectFromDictionaryForProperty = _realDictionary[propertyName];

            // Set our implementation
            IMP imp = [self methodForSelector:propertySetterSelector];

            // Set Dictionary to property
            if (strcmp(typeOfProperty, @encode(id)) == 0) {
                //NSLog(@"Is Object");
                void (*func)(id, SEL, id) = (void *)imp;
                func(self, propertySetterSelector, objectFromDictionaryForProperty);
            }
            else if (strcmp(typeOfProperty, @encode(BOOL)) == 0) {
                //NSLog(@"Is Bool");
                void (*func)(id, SEL, BOOL) = (void *)imp;
                func(self, propertySetterSelector, [objectFromDictionaryForProperty boolValue]);
            }
            else if (strcmp(typeOfProperty, @encode(int)) == 0) {
                //NSLog(@"Is Int");
                void (*func)(id, SEL, int) = (void *)imp;
                func(self, propertySetterSelector, [objectFromDictionaryForProperty intValue]);
            }
            else if (strcmp(typeOfProperty, @encode(float)) == 0) {
                //NSLog(@"Is Float");
                void (*func)(id, SEL, float) = (void *)imp;
                func(self, propertySetterSelector, [objectFromDictionaryForProperty floatValue]);
            }
            else if (strcmp(typeOfProperty, @encode(double)) == 0) {
                //NSLog(@"Is Double");
                void (*func)(id, SEL, double) = (void *)imp;
                func(self, propertySetterSelector, [objectFromDictionaryForProperty doubleValue]);
            }

        }
    }
}

- (SEL) setterSelectorForPropertyName:(NSString *)propertyName {

    /*
     Because apple automatically generates setters to "setPropertyName:", we can use that and return the first argument to get the type of property it is.  That way, we can set it to our plist values.  Custom setters will cause problems.
     */

    // Make our first letter capitalized -  Using this because `capitalizedString` causes issues with camelCase => Camelcase
    NSString * capitalizedPropertyName = [propertyName stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[propertyName substringToIndex:1] capitalizedString]];

    // The name of our auto synthesized setter | Custom setters will cause issues
    NSString * methodString = [NSString stringWithFormat:@"set%@:", capitalizedPropertyName];

    // Set our Selector
    SEL propertySetterSelector = NSSelectorFromString(methodString);

    // Return it
    return propertySetterSelector;
}

- (const char *) typeOfArgumentForSelector:(SEL)selector atIndex:(int)index {
    NSMethodSignature * sig = [self methodSignatureForSelector:selector];
    // Index 0 is object, Index 1 is the selector: arguments start at Index 2
    const char * argType = [sig getArgumentTypeAtIndex:index];
    return argType;
}

The basic logic is this:

  1. Get property name: propertyName

  2. Generate setter: setPropertyName:(someType)propertyName

  3. Find argument type of setter

  4. Call setter with value from dictionary

My idea is that by subclassing and adding your properties, they will automatically be set to the values from your dictionary at runtime. I'm currently using it for easier interaction with Plists, but I think it would be helpful with JSON, or other objects that I would like to interact with as Objective-C objects.

Example

You have a plist:

Property list editor screenshot

You declare your properties in a subclass to match the names of keys:

#import "PlistModel.h"

@interface CustomModel : PlistModel

@property (strong, nonatomic) NSString * StringPropertyKey;
@property (strong, nonatomic) NSDate * DatePropertyKey;
@property (strong, nonatomic) NSArray * ArrayPropertyKey;
@property (strong, nonatomic) NSDictionary * DictionaryPropertyKey;

@property int IntPropertyKey;
@property BOOL BoolPropertyKey;
@property float FloatPropertyKey;

@end

That's it! The values are automatically populated at runtime without any additional code:

[CustomModel plistNamed:@"CustomModel" inBackgroundWithBlock:^(PlistModel *plistModel) {

    CustomModel * customModel = (CustomModel *)plistModel;

    NSLog(@"StringProperty: %@", customModel.StringPropertyKey);
    NSLog(@"DateProperty: %@", customModel.DatePropertyKey);
    NSLog(@"ArrayProperty: %@", customModel.ArrayPropertyKey);
    NSLog(@"DictionaryProperty: %@", customModel.DictionaryPropertyKey);
    NSLog(@"IntProperty: %i", customModel.IntPropertyKey);
    NSLog(@"BoolProperty: %@", customModel.BoolPropertyKey ? @"YES" : @"NO");
    NSLog(@"FloatProperty: %f", customModel.FloatPropertyKey);

}];

You can interact and update your plist via properties, and the plist will be automatically saved for you. If you don't include a plist in your project, it will automatically generate and save a new one with the name provided.

I reverse the process before saving so that the plist / dictionary is automatically updated to values from properties. This way anything you have added or updated in your properties is saved.

Question

I've seen things like this in the past, but I've never seen how it's done. Is there a better way to set unknown properties at runtime without generating a selector?

Will this cause some unforeseen problem?

Full project is available here! I don't have docs ready yet, and there are still probably a few kinks, but if you can piece it together, it's ready to go!

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

This review is going to have to be in the form of a handful of questions, things perhaps you haven't considered. I don't know the answer to these, but I know they need to be answered.

typeOfArgumentForSelector:(SEL)selector atIndex:(int)index and the way you use it is bothering me a little bit. As written, the method is somewhere between fine for reusability and not completely correct for this use. What's clearly missing is a safety net around the call to [sig getArgumentTypeAtIndex:index];

Per the documentation, NSMethodSignature is only guaranteed to have two argument indices, for the two hidden arguments that every Objective-C method has. We have no guarantee that there will be anything at index 2. It could be out of bounds and throw an exception.

You could say, "but I'm only using it to get setters, which always have exactly 1 argument!" and that'd be fair... but if that's the case, the method is poorly named and shouldn't take two arguments.

So as I said, you're floating in between two spots.

If you want the method to remain generic so you can use it as is in other projects, then you need some safety (I'll come back to this). If you would rather the method stay specific to this project, change the method to:

- (const char *)dataTypeForSetter:(SEL)setter;

And before you try grabbing the argument, put this line in:

if ([sig numberOfArguments] != 3) return NULL;

Because if the number of arguments isn't exactly 3, then the method isn't a setter.

Now alternatively, if you want to leave the method more flexible, you can, but you still need to put some safety around your call to getArgumentTypeAtIndex:. There's nothing guaranteeing the index exists, and if it doesn't, you have an unhandled exception.

So...

if (index >= [sig numberOfArguments]) return NULL;

And just document the method as returning NULL when the requested index greater than the number of arguments.


Beyond this, I see a couple problems that you probably didn't think about while implementing this.

First of all, properties marked as readonly in the public header file but then redefined as readwrite in in the .m file.

If the property is only readonly and never readwrite, most likely there's not even a backing instance variable, but the object will return NO for the respondsToSelector call. HOWEVER, if it is redefined as readwrite, it will return YES to the respondsToSelector call, and will attempt to set this property. This may or may not be the intended behavior, but either way it should certainly be tested and well documented.


Second, consider how most @property setters actually look:

- (void)setSomeIvar:(int)someIvar {
    _someIvar = someIvar;
}

Now consider how most @property getters actually look:

- (int)someIvar {
    return _someIvar;
}

In this case, this is completely unproblematic. The property is straight forward.

Now consider a class where perhaps we're doing something with angles, and we've got a non-standard setter/getter.

- (void)setDegrees:(int)degrees {
    _degrees = degrees % 360;
    _revolutions = degrees / 360;
}

- (int)degrees {
    return _degrees;
}

- (int)revolutions {
    return _revolutions;
}

Now... I'm not arguing that this is good design for a class. Personally, I'd do the opposite and store everything in degrees and calculate revolutions when it's called. But the point here is that this is a perfectly possible class set up. And for argument's sake, we're going to assume that revolutions is readonly @property (so it won't have its own setter).

If I originally set degrees to 390, I'll have 1 revolution and 30 degrees. If your code saves it to a plist, it will save it as such, and then when it tries to write it via the plist, it will end up setting the values to 0 revolutions and 30 degrees, which is different from what was originally in there.

The real point here is that in this example:

NSLog(@"A: %@", self.iVar);
[self setIvar:[self iVar]];
NSLog(@"B: %@", self.iVar);

A and B here are not guaranteed to log exactly the same, depending on the implementation of the setter and getter.

\$\endgroup\$
5
  • \$\begingroup\$ 1. I'll put in an exception for if an argument doesn't exist, just so the method can feel more resiliant. 2. I mention a couple times, and will mention in docs that you have to use autosynthesized getters setters. I will also mention not to use readonly properties until I can come up with a solution. Thanks for pointing out a couple possible contingencies that I should account for. \$\endgroup\$
    – Logan
    Commented May 1, 2014 at 23:11
  • \$\begingroup\$ Code review should have larger comments so I can respond w/ code etc.! Responses in comments are always difficult. \$\endgroup\$
    – Logan
    Commented May 1, 2014 at 23:11
  • \$\begingroup\$ You can use readwrite property. While readwrite is a property attribute, it is only the default property attribute. Every property is either readonly or readwrite. \$\endgroup\$
    – nhgrif
    Commented May 1, 2014 at 23:12
  • 1
    \$\begingroup\$ There are chat rooms: chat.stackexchange.com/rooms/12918/nschat \$\endgroup\$
    – nhgrif
    Commented May 1, 2014 at 23:12
  • \$\begingroup\$ Ahh, I didn't mean to say readwrite, I edited my comment! Thanks \$\endgroup\$
    – Logan
    Commented May 1, 2014 at 23:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.