Search code examples
objective-cdynamickey-value-coding

iOS - Runtime Dynamic Class not key-value coding compliant


I've been getting up to speed on Dynamic Class creation at runtime in Objective-C and have hit an issue I can't quite seem to work around. After a good deal of searching around, I'm still puzzled.

The issue here is making a:

[runtimeClassInstance setValue:age forKey:@"age"];

call to an object instantiated from:

id runtimeClassInstance = [[NSClassFromString(@"Employee") alloc] init];

Which was created by

[NewClassMaker buildClassFromDictionary:@[@"FirstName", @"LastName",\
                                          @"Age", @"Salary"]
                               withName:@"Employee"];

This is croaking with an NSUnknownKeyException:this class is not key value coding-compliant for the key age error.

If I use the following invocation approach, things work just fine:

SEL setAgeSelector = NSSelectorFromString(@"setAge");
NSInvocation *call = [NSInvocation invocationWithMethodSignature:[[NSClassFromString(@"Employee") class] instanceMethodSignatureForSelector:setAgeSelector]];
call.target = runtimeClassInstance;
call.selector = setAgeSelector;
NSNumber *age = @(25);
[call setArgument:&age atIndex:2];
[call invoke];

Now that static method to create the class is as such:

+(NSDictionary*)buildClassFromDictionary:(NSArray*)propNames withName:(NSString*)className
{
    NSMutableDictionary* keys = [[NSMutableDictionary alloc]init];
    Class newClass = NSClassFromString(className);

    if(newClass == nil)
    {
        newClass = objc_allocateClassPair([NSObject class], [className UTF8String], 0);

        // For each property name, add a new iVar, getter, and setter method for it.
        for(NSString* key in propNames)
        {
            NSString* propName = [self propName: key];
            NSString* iVarName = [self ivarName:propName];

            class_addIvar(newClass, [iVarName UTF8String] , sizeof(NSObject*), log2(sizeof(NSObject*)), @encode(NSObject));

            objc_property_attribute_t a1 = { "T", "@\"NSObject\"" };
            objc_property_attribute_t a2 = { "&", "" };
            objc_property_attribute_t a3 = { "N", "" };
            objc_property_attribute_t a4 = { "V", [iVarName UTF8String] };
            objc_property_attribute_t attrs[] = { a1, a2, a3, a4};

            class_addProperty(newClass, [propName UTF8String], attrs, 4);
            class_addMethod(newClass, NSSelectorFromString(propName), (IMP)getter, "@@:");
            class_addMethod(newClass, NSSelectorFromString([self setterName:propName]), (IMP)setter, "v@:@");

            [keys setValue:key forKey:propName];
        }

        Class metaClass = object_getClass(newClass);
        // This method is returning NO on purpose to find out why there is no key.
        class_addMethod(metaClass, @selector(accessInstanceVariablesDirectly), (IMP)accessInstanceVariablesDirectly, "B@:");

        // Auxilliary methods added to the new class instance so the accessor dynamic methods above can work.
        // Not sure if the initial impl of this class maker class worked.
        class_addMethod(newClass, @selector(ivarName:), (IMP)ivarNameForString, "@@:@");
        class_addMethod(newClass, @selector(propName:), (IMP)propNameForString, "@@:@");
        class_addMethod(newClass, @selector(propNameFromSetterName:), (IMP)propNameFromSetterNameString, "@@:@");

        // Add a customized description dynamic method to this class. It will dump out any list of properties added
        // to the object during init here.
        Method description = class_getInstanceMethod([NSObject class],
                                                     @selector(description));
        const char *types = method_getTypeEncoding(description);

        // now add
        class_addMethod(newClass, @selector(description), (IMP)Description, types);
        objc_registerClassPair(newClass);
    }
    return keys;
}

There are three possibilities here that I am stewing over:

  1. I have missed some crucial step here to get these properties and their dynamic accessors working in a key-value compliant fashion
  2. If I do allow direct iVar access, is the retain for the value not happening?
  3. Is the NSInvocation approach the best way?

Solution

  • The setter method is named setAge. setValue:forKey: searches for setAge: with a colon. Add a colon at the end of the name of the setter method. In Objective-C the colons are part of the method names.