Assigning a SEL Property Using KVC

ARC enabled.

I have a class with a property of type SEL: @property SEL mySelector;

Synthesized: @synthesize mySelector;

Then I try to assign a value to it using KVC:

 SEL someSelector = @selector(doSomething:) NSValue* someSelectorValue = [NSValue value:someSelector withObjCType:@encode(SEL)]; [target setValue:someSelectorValue forKey:@"mySelector"]; 

I get an error message:

 [<LACMyClass 0x101b04bc0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key mySelector. 

This is obviously not true: the class is KVC-compliant, it just doesn't like the value I'm passing. It works when I define a property of type void* instead of SEL , but that does not meet my requirements.

In addition to using value:withObjCType: I also tried valueWithBytes:objCType: and valueWithPointer:

Can anyone explain me

  • What happens and
  • How to do it right?
+6
source share
2 answers

It seems that only a specific subset of primitive types is supported for automatic boxing / unpacking by default setValue:forKey: See Table 1 and Table 2 in the "Scalar and Structural Support" chapter of the "Key Code Programming Guide " chapter . It is understood here that only BOOL , char , double , float , int , long , long long , short and their unsigned copies are fully supported along with struct via NSValue . Other types, such as SEL and other pointer values, are unsupported .

Consider the following program:

 #import <Foundation/Foundation.h> @interface MyObject : NSObject @property (nonatomic) SEL mySelector; @property (nonatomic) void *myVoid; @property (nonatomic) int myInt; @property (nonatomic,unsafe_unretained) id myObject; @end @implementation MyObject @end int main(int argc, char *argv[]) { @autoreleasepool { SEL selector = @selector(description); NSValue *selectorValue = [NSValue valueWithPointer:selector]; NSValue *voidValue = [NSValue valueWithPointer:selector]; NSValue *intValue = @1; __unsafe_unretained id obj = (__bridge id)(const void *)selector; MyObject *object = [[MyObject alloc] init]; // The following two calls succeed: [object setValue:intValue forKey:@"myInt"]; [object setValue:obj forKey:@"myObject"]; // These two throw an exception: [object setValue:voidValue forUndefinedKey:@"myVoid"]; [object setValue:selectorValue forKey:@"mySelector"]; } } 

We can easily set the int and id properties - even using __unsafe_unretained and bridge translations so that we can pass the value of the selector. However, trying to set either of the two types of pointers is not supported.

How do we get out of here? For example, we could override valueForKey: and setValueForKey: in MyObject to support decompression of SEL types or to intercept a specific key. An example of the latter approach:

 @implementation MyObject - (id)valueForKey:(NSString *)key { if ([key isEqualToString:@"mySelector"]) { return [NSValue valueWithPointer:self.mySelector]; } return [super valueForKey:key]; } - (void)setValue:(id)value forKey:(NSString *)key { if ([key isEqualToString:@"mySelector"]) { SEL toSet; [(NSValue *)value getValue:&toSet]; self.mySelector = toSet; } else { [super setValue:value forUndefinedKey:key]; } } @end 

When used, we find that it works as expected:

 [object setValue:selectorValue forKey:@"mySelector"]; NSString *string = NSStringFromSelector(object.mySelector); NSLog(@"selector string = %@", string); 

This log displays "selector string = description" on the console.

Of course, this is due to maintainability, since now you need to implement these methods in each class, you need to install selectors with KVC, and you will also have to compare it with hard-coded keys. One way that is risky is to use the swizzling method and replace the replacement of NSObject implementation of the KVC methods of our own, which handle the boxing and unpacking of SEL types.

The following program, built on the first example, is very different from Mike Ash's brilliant let build KVC , and also uses the Swizzle() function from this answer to SO . Note that I cut corners for the purpose of demonstration, and that this code will only work with SEL attributes that have getters and setters named accordingly, and will not directly check instance variables, unlike the default KVC implementations.

 #import <Foundation/Foundation.h> #import <objc/runtime.h> @interface MyObject : NSObject @property (nonatomic) SEL mySelector; @property (nonatomic) int myInt; @end @implementation MyObject @end @interface NSObject (ShadyCategory) @end @implementation NSObject (ShadyCategory) // Implementations of shadyValueForKey: and shadySetValue:forKey: Adapted from Mike Ash "Let Build KVC" article // http://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html // Original MAObject implementation on github at https://github.com/mikeash/MAObject - (id)shadyValueForKey:(NSString *)key { SEL getterSEL = NSSelectorFromString(key); if ([self respondsToSelector: getterSEL]) { NSMethodSignature *sig = [self methodSignatureForSelector: getterSEL]; char type = [sig methodReturnType][0]; IMP imp = [self methodForSelector: getterSEL]; if (type == @encode(SEL)[0]) { return [NSValue valueWithPointer:((SEL (*)(id, SEL))imp)(self, getterSEL)]; } } // We will have swapped implementations here, so this call NSObject valueForKey: method return [self shadyValueForKey:key]; } - (void)shadySetValue:(id)value forKey:(NSString *)key { NSString *capitalizedKey = [[[key substringToIndex:1] uppercaseString] stringByAppendingString:[key substringFromIndex:1]]; NSString *setterName = [NSString stringWithFormat: @"set%@:", capitalizedKey]; SEL setterSEL = NSSelectorFromString(setterName); if ([self respondsToSelector: setterSEL]) { NSMethodSignature *sig = [self methodSignatureForSelector: setterSEL]; char type = [sig getArgumentTypeAtIndex: 2][0]; IMP imp = [self methodForSelector: setterSEL]; if (type == @encode(SEL)[0]) { SEL toSet; [(NSValue *)value getValue:&toSet]; ((void (*)(id, SEL, SEL))imp)(self, setterSEL, toSet); return; } } [self shadySetValue:value forKey:key]; } @end // Copied from: https://stackoverflow.com/a/1638940/475052 void Swizzle(Class c, SEL orig, SEL new) { Method origMethod = class_getInstanceMethod(c, orig); Method newMethod = class_getInstanceMethod(c, new); if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); else method_exchangeImplementations(origMethod, newMethod); } int main(int argc, char *argv[]) { @autoreleasepool { Swizzle([NSObject class], @selector(valueForKey:), @selector(shadyValueForKey:)); Swizzle([NSObject class], @selector(setValue:forKey:), @selector(shadySetValue:forKey:)); SEL selector = @selector(description); MyObject *object = [[MyObject alloc] init]; object.mySelector = selector; SEL fromProperty = object.mySelector; NSString *fromPropertyString = NSStringFromSelector(fromProperty); NSValue *fromKVCValue = [object valueForKey:@"mySelector"]; SEL fromKVC; [fromKVCValue getValue:&fromKVC]; NSString *fromKVCString = NSStringFromSelector(fromKVC); NSLog(@"fromProperty = %@ fromKVC = %@", fromPropertyString, fromKVCString); object.myInt = 1; NSNumber *myIntFromKVCNumber = [object valueForKey:@"myInt"]; int myIntFromKVC = [myIntFromKVCNumber intValue]; int myIntFromProperty = object.myInt; NSLog(@"int from kvc = %d from propety = %d", myIntFromKVC, myIntFromProperty); selector = @selector(class); NSValue *selectorValue = [NSValue valueWithPointer:selector]; [object setValue:selectorValue forKey:@"mySelector"]; SEL afterSettingWithKVC = object.mySelector; NSLog(@"after setting the selector with KVC: %@", NSStringFromSelector(afterSettingWithKVC)); [object setValue:@42 forKey:@"myInt"]; int myIntAfterSettingWithKVC = object.myInt; NSLog(@"after setting the int with KVC: %d", myIntAfterSettingWithKVC); } } 

The result of this program demonstrates the capabilities of boxing and unboxing:

 2013-08-30 19:37:14.287 KVCSelector[69452:303] fromProperty = description fromKVC = description 2013-08-30 19:37:14.288 KVCSelector[69452:303] int from kvc = 1 from propety = 1 2013-08-30 19:37:14.289 KVCSelector[69452:303] after setting the selector with KVC: class 2013-08-30 19:37:14.289 KVCSelector[69452:303] after setting the int with KVC: 42 

Swizzling, of course, is not without risk, so proceed with caution!

+7
source

The error is trying to tell you that the class is not suitable for the mySelector key.

 STAssertNoThrow([target setValue:nil forKey:@"mySelector"], nil); 

also does not work, the problem with mySelector not with a SEL boxing.


I was curious, so I just checked this test:

 @interface KVCOnSELTester : NSObject @property (assign, nonatomic) SEL mySelector; @property (assign, nonatomic) int myInteger; @end @implementation KVCOnSELTester @end @implementation KVCOnSELTests - (void)testKVCOnSEL { KVCOnSELTester *target = [KVCOnSELTester new]; STAssertNoThrow((target.myInteger = 1), nil); STAssertNoThrow([target setMyInteger:1], nil); STAssertNoThrow([target setValue:nil forKey:@"myInteger"], nil); STAssertNoThrow((target.mySelector = @selector(setUp)), nil); STAssertNoThrow([target setMySelector:@selector(setUp)], nil); STAssertNoThrow([target setValue:nil forKey:@"mySelector"], nil); } @end 

STAssertNoThrow([target setValue:nil forKey:@"myInteger"], nil) threw [target setValue:nil forKey:@"myInteger"] raised [<KVCOnSELTester 0x8a74840> setNilValueForKey]: could not set nil as the value for the key myInteger..

This correctly states that nil not suitable for myInteger .

STAssertNoThrow([target setValue:nil forKey:@"mySelector"], nil) threw [target setValue:nil forKey:@"mySelector"] raised [<KVCOnSELTester 0x8a74840> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key mySelector..

This is the same mistake as before. I think SEL is just not KVC. I tried to see it, but it wasn’t anywhere.

+2
source

Source: https://habr.com/ru/post/952858/


All Articles