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:
Get property name:
propertyName
Generate setter:
setPropertyName:(someType)propertyName
Find argument type of setter
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:
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!