Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assigning to a property of type SEL using KVC

ARC is enabled.

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

It is 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 the 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 does not like the value that I am passing in. It appears to work when I define the property of type void* instead of SEL, but that does not fulfill my requirements.

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

Could anyone explain to me

  1. What is going on, and
  2. How to do this properly?
like image 474
Brandon Horst Avatar asked Aug 30 '13 23:08

Brandon Horst


2 Answers

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!

like image 188
Carl Veazey Avatar answered Nov 18 '22 11:11

Carl Veazey


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

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

should also fail, the trouble is with mySelector not with boxing a SEL.


I was curious, so I just ran 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 is not proper 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 error as you got before. I think SEL is just not KVC. I've tried looking this up but I got nowhere.

like image 2
Jeffery Thomas Avatar answered Nov 18 '22 09:11

Jeffery Thomas