patternMinor
Dynamically setting unknown properties of a subclass from parent class in Objective-C
Viewed 0 times
dynamicallyunknownpropertiesparentsubclasssettingobjectivefromclass
Problem
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:
And here's what happens:
```
// 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)
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)
Solution
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.
Per the documentation,
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:
And before you try grabbing the argument, put this line in:
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
So...
And just document the method as returning
Beyond this, I see a couple problems that you probably didn't think about while implementing this.
First of all, properties marked as
If the property is only
Second, consider how most
Now consider how most
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.
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
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:
A and B here are not guaranteed to log exactly the same, depending on the implementation of the setter and getter.
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.
Code Snippets
- (const char *)dataTypeForSetter:(SEL)setter;if ([sig numberOfArguments] != 3) return NULL;if (index >= [sig numberOfArguments]) return NULL;- (void)setSomeIvar:(int)someIvar {
_someIvar = someIvar;
}- (int)someIvar {
return _someIvar;
}Context
StackExchange Code Review Q#48703, answer score: 4
Revisions (0)
No revisions yet.