2013-08-30 10 views
6

ŁUK jest włączone.Przypisywanie do właściwości typu SEL przy użyciu KVC

Mam klasy z właściwością typu SEL: @property SEL mySelector;

To jest syntetyzowany: @synthesize mySelector;

Następnie próbuję przypisać wartość do niego używając KVC:

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

otrzymuję komunikat o błędzie:

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

To jest ob viously not true - klasa jest zgodna z KVC, po prostu nie podoba mi się wartość, którą przekazuję. Wydaje się działać, gdy zdefiniuję właściwość typu void* zamiast SEL, ale to nie spełnia moich wymagań.

Oprócz korzystania value:withObjCType:, ja też próbowałem valueWithBytes:objCType: i valueWithPointer:

Czy ktoś może mi wyjaśnić

  1. Co się dzieje, i
  2. Jak to zrobić poprawnie?

Odpowiedz

7

Wygląda na to, że tylko pewien podzbiór typów pierwotnych jest obsługiwany dla automatycznego boksu/rozpakowywania według domyślnej implementacji setValue:forKey:. Patrz Tabela 1 i Tabela 2 z "Scalar and Structure Support" chapter "Podręcznika programowania kodowania wartości klucza". Konsekwencją jest to, że tylko BOOL, char, double, float, int, long, long long, short oraz ich odpowiedniki bez znaku są w pełni obsługiwane, wraz z struct s poprzez NSValue. Inne typy, takie jak SEL i inne wartości wskaźników, appear to be unsupported.

Rozważmy następujący 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"]; 
    } 
} 

Możemy ustawić int i id właściwości dobrze - nawet przy użyciu __unsafe_unretained i mostkiem odlewane pozwolić nam przekazać wartość selektora. Jednak próba ustawienia jednego z dwóch typów wskaźników nie jest obsługiwana.

Jak przejść dalej? Możemy na przykład zastąpić valueForKey: i setValueForKey: w MyObject w celu obsługi rozpakowywania typów SEL lub przechwycenia określonego klucza. Przykładem tego ostatniego podejścia:

@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 

W praktyce, możemy znaleźć to działa zgodnie z oczekiwaniami:

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

ten rejestruje "selektor String = opisem" do konsoli.

Oczywiście ma to problemy z konserwacją, ponieważ teraz musisz zaimplementować te metody w każdej klasie, musisz ustawić selektory za pomocą KVC, a także musisz porównać z kluczami zakodowanymi na stałe. Jednym ze sposobów obejścia tego, co jest ryzykowne, jest zastosowanie metody swizzling i zastąpienie zastąpień NSObject implementacji metod KVC własnymi, które obsługują boksowanie i rozpakowywanie typów SEL.

Poniższy program, zbudowany na pierwszym przykładzie, w dużej mierze wywodzi się z genialnego artykułu Mike'a Ashta o "let's build KVC", a także korzysta z funkcji Swizzle() z this answer on SO. Zauważ, że wycinam rogi w celu demonstracji i że ten kod będzie działał tylko z atrybutami SEL, które mają odpowiednio nazwane obiekty pobierające i ustawiające i nie będzie bezpośrednio sprawdzać zmiennych instancji, w przeciwieństwie do domyślnych implementacji KVC.

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

Wyjście z tego programu pokazuje swój boks i unboxing możliwości:

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 nie jest oczywiście bez ryzyka, więc postępować ostrożnie!

+0

Wygląda na to, że SEL po prostu nie jest kompatybilny z boxem/unboxingiem. Odpowiedź Jeffery'ego Thomasa była równie słuszna, ale to obejście jest pomocne. Zastanawiam się jednak, czy możliwe jest jakikolwiek prawdziwy boxing/unboxing. –

+0

@BrandonHorst Dzięki. Zobacz moją edycję - dodałem bardziej ogólne obejście, jeśli jesteś zainteresowany. –

2

Błąd próbuje powiedzieć, że klasa nie jest zgodna z kluczem mySelector.

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

powinny również nie, problem jest z mySelector nie z boks SEL.


Byłem ciekaw, więc po prostu zabrakło tego testu:

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

to słusznie stwierdza, że ​​nil nie jest właściwe dla myInteger.

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

Jest to ten sam błąd jak masz wcześniej. Myślę, że SEL nie jest po prostu KVC. Próbowałem już to sprawdzić, ale nie udało mi się.

Powiązane problemy