Search code examples
objective-ckey-value-observing

Key/value observing not firing for property defined as: @property (readwrite, retain) __attribute__((NSObject)) CGImageRef thumbnailImage


I have a class with a property definition like so:

// Interface
@property (retain) __attribute__((NSObject)) CGImageRef thumbnailImage;

// Implementation
@synthesize thumbnailImage;

The problem is that self.thumbnailImage = newCGImageRef; does not cause it to fire a key/value observation event and observeValueForKeyPath:... is never called.

I tested the code with another property (BOOL) of the same class and it works just fine for that one. Shouldn't this also work for the property above?

Note: I tried setting the property manually ([self setValue:newValue forKey:@"thumbnailImage"], which resulted in the error below:

setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key thumbnailImage.

Solution

  • __attribute__((NSObject)) tells the compiler to treat structure pointers as an object therefore this allows you to retain/release the object (often used for blocks). This attribute has no effect on the fact that CFImageRef is a struct pointer and KVC does not automatically wrap struct pointers though it will wrap structs. To solve your issue you will need to intercept KVC on undefined keys to set and return the correct object, or just use UIImage instead.

    KVC Documentation:

    Automatic wrapping and unwrapping is not confined to NSPoint, NSRange, NSRect, and NSSize—structure types (that is, types whose Objective-C type encoding strings start with {) can be wrapped in an NSValue object.

    TestCF_KVC.h

    #import <Foundation/Foundation.h>
    
    typedef struct
    {
        int x, y, z;
    } AStruct;
    
    @interface TestCF_KVC : NSObject {}
    
    @property (nonatomic, retain) NSString *text;
    @property (nonatomic, assign) AStruct aStruct;
    @property (nonatomic, retain) __attribute__((NSObject)) CFStringRef cftext;
    
    -(void)test;
    
    @end
    

    TestCF_KVC.m

    #import "TestCF_KVC.h"
    
    @implementation TestCF_KVC
    
    @synthesize text, aStruct, cftext;
    
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key
    {
        NSLog(@"undefined key set for %@", key);
        if([key isEqualToString:@"cftext"])
        {
            self.cftext = (CFStringRef)value;
        }
        else
        {
            [super setValue:value forUndefinedKey:key];
        }
    }
    
    -(id)valueForUndefinedKey:(NSString *)key
    {
        NSLog(@"undefined key get for %@", key);
        if([key isEqualToString:@"cftext"])
        {
            return (NSString*)self.cftext;
        }
    
        return [super valueForUndefinedKey:key];
    }
    
    -(void)test
    {
        NSString *txt = @"text worked";
        AStruct astr = { .x=1, .y=5, .z=10 };
        CFStringRef cftxt = (CFStringRef)@"cftext worked";
    
        //Test a normal NSString for KVC
        [self setValue:txt forKey:@"text"];
        txt = [self valueForKey:@"text"];
        NSLog(@"text[%s]: %@", @encode(NSString), [self valueForKey:@"text"]);
    
        //Test a struct for KVC
        NSValue *value = [NSValue value:&astr withObjCType:@encode(AStruct)];
        [self setValue:value forKey:@"aStruct"];
        [[self valueForKey:@"aStruct"] getValue:&astr];
        NSLog(@"aStruct[%s]: %d %d %d", @encode(AStruct), aStruct.x, aStruct.y, aStruct.z);
    
        //Test a Core Foundation type for KVC
        [self setValue:(NSString*)cftxt forKey:@"cftext"];
        cftxt = (CFStringRef)[self valueForKey:@"cftext"];
        NSLog(@"cftext[%s]: %@", @encode(CFStringRef), (NSString*)cftxt);
    }
    
    @end
    

    Log Output From Calling -test:

    text[{NSString=#}]: text worked
    aStruct[{?=iii}]: 1 5 10
    undefined key set for cftext
    undefined key get for cftext
    cftext[^{__CFString=}]: cftext worked