使用KVC分配给SEL类型的属性

时间:2013-08-30 23:03:08

标签: objective-c macos cocoa

启用ARC。

我有一个类型为SEL的属性:@property SEL mySelector;

它被合成:@synthesize mySelector;

然后我尝试使用KVC为其分配一个值:

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

我收到错误消息:

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

这显然不正确 - 该类符合KVC,它只是不喜欢我传入的值。当我定义类型void*的属性而不是{{1但是,这不符合我的要求。

除了使用SEL之外,我还尝试了value:withObjCType:valueWithBytes:objCType:

有人可以向我解释

  1. 发生了什么,
  2. 如何正确地做到这一点?

2 个答案:

答案 0 :(得分:7)

默认setValue:forKey:实现似乎只支持自动装箱/拆箱的原始类型的某个子集。请参阅“键 - 值编码编程指南”的"Scalar and Structure Support" chapter中的表1和表2。这里的含义是只有BOOLchardoublefloatintlonglong long,{{ 1}},以及short通过struct完全支持他们的未签名对应方。其他类型,例如NSValue和其他指针值appear to be unsupported

考虑以下计划:

SEL

我们可以设置#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"]; } } int属性,即使使用id和桥接强制转换也可以让我们传递选择器值。但是,尝试设置两种指针类型中的任何一种都不受支持。

我们如何从这里开始?例如,我们可以覆盖__unsafe_unretained中的valueForKey:setValueForKey:,以支持取消装箱MyObject类型或截取特定密钥。后一种方法的一个例子:

SEL

在使用中,我们发现它按预期工作:

@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

这会将“selector string = description”记录到控制台。

当然,这有可维护性问题,因为您现在必须在使用KVC设置选择器所需的每个类中实现这些方法,并且还必须与硬编码键进行比较。解决这个风险的方法之一就是使用方法调配并用我们自己的方法替换[object setValue:selectorValue forKey:@"mySelector"]; NSString *string = NSStringFromSelector(object.mySelector); NSLog(@"selector string = %@", string); 的KVC方法的实现,它们可以处理NSObject类型的装箱和拆箱。

以第一个例子为基础的以下程序源自Mike Ash的精彩"let's build KVC"文章,并使用this answer on SO中的SEL函数。请注意,我为了演示而偷工减料,并且此代码仅适用于具有适当命名的getter和setter的Swizzle()属性,并且不会直接检查实例变量,这与默认的KVC实现不同。

SEL

此程序的输出演示了其装箱和拆箱功能:

#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);
    }
}

调酒当然不是没有风险,所以要小心谨慎!

答案 1 :(得分:2)

错误是试图告诉您该类与mySelector键不兼容。

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

也应该失败,麻烦在于mySelector而不是拳击SEL


我很好奇,所以我就跑了这个测试:

@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)扔了[target setValue:nil forKey:@"myInteger"] raised [<KVCOnSELTester 0x8a74840> setNilValueForKey]: could not set nil as the value for the key myInteger..

这正确地指出nil不适合myInteger

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

这与您之前的错误相同。我认为SEL不是KVC。我已经尝试过了,但我无处可去。