Reading union properties using KVC in Objective-C

368 views Asked by At

Update:

I have boiled the issue down to simply not being able to use key value coding on a class I made seen below

#import <Foundation/Foundation.h>
#import <GLKit/GLKit.h>

@interface CMTransformation : NSObject

@property(nonatomic) GLKVector3 position;
@property(nonatomic) GLKVector3 scale;
@property(nonatomic) GLKVector3 rotation;
@property(nonatomic) GLKVector3 anchor;

@property(nonatomic) GLKMatrix4 matrix;

- (GLKMatrix4)calculateMatrixWithParentTransformation:(CMTransformation *)parentTransformation;

@end

It has been my understanding and expience that I should be able to grab non NSObjects out as NSValues, however, I am finding it impossible to access these items (which are defined as unions) using KVC syntax:

CMTransformation* trans = [[CMTransformation alloc] init];
temp = [trans valueForKey:@"position"];

Similarly, if I try to access the underlying variable:

CMTransformation* trans = [[CMTransformation alloc] init];
temp = [trans valueForKey:@"_position"];

Both of these throw an exception because the key is not found. What am I missing here?

Previous Question

I have written some code which allows me to access a (somewhat) arbitrary structure with a string such as "transformation.position"

For some reason the code stops working on the second jump when I am trying to read a property from an NSObject. Here is the

NSString* property = actionDetails[@"Property"];
PropertyParts = [[property componentsSeparatedByString:@"."] mutableCopy];
int count = [PropertyParts count];
id current_object = initial_object;

for(int i = 0; i < count; i++)
{
    NSString* current_part = PropertyParts[i];        
    current_object = [current_object valueForKey:current_part];
}

I have tried all possible syntax for property access including Property, property and _property.

Here is the custom NSObject declaration

#import <Foundation/Foundation.h>
#import <GLKit/GLKit.h>

@interface CMTransformation : NSObject

@property(nonatomic) GLKVector3 position;
@property(nonatomic) GLKVector3 scale;
@property(nonatomic) GLKVector3 rotation;
@property(nonatomic) GLKVector3 anchor;

@property(nonatomic) GLKMatrix4 matrix;

- (GLKMatrix4)calculateMatrixWithParentTransformation:(CMTransformation *)parentTransformation;

@end

Additionally, I can see after the first loop that the debugger says that CMTransformation* is populating currrent_object, so I am at a loss as to why I can't access its properties?

2

There are 2 answers

3
nielsbot On BEST ANSWER

Would you be satisfied with implementing valueForUndefinedKey: and setValue:forUndefinedKey:?

If so, this works:

union Test
{
    CGFloat f ;
    NSInteger i ;
};

@interface TestClass : NSObject
@property ( nonatomic ) union Test t ;
@end

@implementation TestClass

-(void)setValue:(nullable id)value forUndefinedKey:(nonnull NSString *)key
{
    Ivar v = class_getInstanceVariable( [ self class ], [ [ NSString stringWithFormat:@"_%@", key ] UTF8String ] ) ;
    if ( v )
    {
        char const * const encoding = ivar_getTypeEncoding( v ) ;
        if ( encoding[0] == '(' ) // unions only
        {
            size_t size = 0 ;
            NSGetSizeAndAlignment( encoding, &size, NULL ) ;

            uintptr_t ptr = (uintptr_t)self + ivar_getOffset( v ) ;
            [ (NSValue*)value getValue:(void*)ptr ] ;

            return ;
        }
    }

    objc_property_t prop = class_getProperty( [ self class ], [ key UTF8String ] ) ;
    if ( prop )
    {
        char const * const encoding = property_copyAttributeValue( prop, "T" ) ;
        if ( encoding[0] == '(' )   // unions only
        {
            objc_setAssociatedObject( self, NSSelectorFromString( key ), value, OBJC_ASSOCIATION_COPY ) ;
            return ;
        }
    }

    [ super setValue:value forUndefinedKey:key ] ;
}

-(nullable id)valueForUndefinedKey:(nonnull NSString *)key
{
    Ivar v = class_getInstanceVariable( [ self class ], [ [ NSString stringWithFormat:@"_%@", key ] UTF8String ] ) ;
    if ( v )
    {
        char const * const encoding = ivar_getTypeEncoding( v ) ;
        if ( encoding[0] == '(' )
        {
            size_t size = 0 ;
            NSGetSizeAndAlignment( encoding, &size, NULL ) ;

            uintptr_t ptr = (uintptr_t)self + ivar_getOffset( v ) ;
            NSValue * result = [ NSValue valueWithBytes:(void*)ptr objCType:encoding ] ;
            return result ;
        }
    }

    objc_property_t prop = class_getProperty( [ self class ], [ key UTF8String ] ) ;
    if ( prop )
    {
        return objc_getAssociatedObject( self, NSSelectorFromString( key ) ) ;
    }

    return [ super valueForUndefinedKey:key ] ;
}

@end


int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        union Test u0 = { .i = 1234 } ;
        TestClass * const testClass = [ TestClass new ] ;

        [ testClass setValue:[ NSValue valueWithBytes:&u0 objCType:@encode( typeof( u0 ) ) ] forKey:@"t" ] ;
        assert( testClass.t.i == 1234 ) ;

        NSValue * const result = [ testClass valueForKey:@"t" ] ;

        union Test u1 ;
        [ result getValue:&u1 ] ;

        assert( u1.i == 1234 ) ;
    }
    return 0;
}

(Paste it into your main.m file to try)

1
Rob Napier On

I am fairly certain KVC does not work with C unions. See bbum's discussion in Objective-C KVO doesn't work with C unions. I know this is KVO-related, but he says:

Oooh... neat. KVO w/unions simply doesn't work. It appears that the runtime simply does not even recognize that the class has a key called vectorUnionValue at all.

That is likely your problem. You will likely need to make some other property to access them. Perhaps something like:

@property(nonatomic) GLKVector3 positionValue;

- (NSValue *)positionValue {
    return [NSValue valueWithPointer:&_positionValue];
}

I don't love that; and there may be a better approach, but I suspect you'll need something along these lines. You likely just cannot get to unions this way.


This is built into KVC. If you have an object that has a transformation object property, and that object has a position property, then the path transformation.position will return it. There is no need to break it up yourself.

But transformation must be a object itself that conforms to key-value coding. There are rules for conforming to KVC, and your irregular random naming of variables might be breaking that. Cocoa variables and properties should be leading-lower camelcase. You should never have a leading cap (that's a class), and you should avoid internal underscores. KVC uses this naming convention to allow it to automatically find properties. For example, it can use that to know that the setter for foo is setFoo: (not the capitalization).

And all the intermediate levels must be objects. For example, GLKVector3 is not an object, so you can't use KVC to interrogate it's subcomponents. You have to return the entire vector. You can unfortunately only know whether GLKVector3 is an object or not by consulting the header file or the docs (or by having an intuition about it based on experience; I would never expect that to be an object because it's a "mathy thing" with are generally structs; this one is a union of structs).