It seems only a certain subset of primitive types are supported for automatic boxing/unboxing by the default setValue:forKey: implementation. See Table 1 and Table 2 from the "Scalar and Structure Support" chapter of the "Key-Value Coding Programming Guide". The implication here is that only BOOL, char, double, float, int, long, long long, short, and their unsigned counterparts are fully supported, along with structs via NSValue. Other types, such as SEL and other pointer values, appear to be 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 set the int and id properties just fine - even using __unsafe_unretained and bridged casts to let us pass the selector value. However, attempting to set either of the two pointer types is unsupported.
How do we proceed from here? We could, for instance, override valueForKey: and setValueForKey: in MyObject to support unboxing either SEL types or to intercept the particular 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
In use, we find it works as expected:
[object setValue:selectorValue forKey:@"mySelector"];
NSString *string = NSStringFromSelector(object.mySelector);
NSLog(@"selector string = %@", string);
This logs "selector string = description" to the console.
Of course, this has maintainability concerns, as you now have to implement these methods in every class you need to set selectors with KVC, and you also have to compare against hard-coded keys. One way around this, which is risky, is to use method swizzling and replace replace NSObject's implementations of KVC methods with our own, which do handle boxing and unboxing of SEL types.
The following program, built upon the first example, derives heavily from Mike Ash's brilliant "let's build KVC" article, and also uses the Swizzle() function from this answer on SO. Note that I cut corners for the purpose of demonstration, and that this code will only work with SEL attributes that have appropriately named getters and setters, and will not directly check for 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's "Let's 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's NSObject's 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 output of this program demonstrates its boxing and unboxing capabilities:
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 is of course not without risk, so proceed with care!