Instance run-time defined objects dynamically in Objective-C

In C# I wrote a nice little feature that allowed users to write custom C# script files that the application's runtime would compile, load and instance during the applications life-cycle. The app even supported modifying or creating new scripts and compiling them, loading and instancing without ever re-starting the application. I did most of this with reflection so it wasn't to hard.

I started thinking about it tonight and thought that it would be neat to implement something like that in Objective-C, just for fun. The goal was to accept a user defined class name, check if it exists and instance it. Once it is instanced, allow for invoking the classes methods. All at runtime.

Another nice thing we could do, is check if the object implements a specific protocol. If you are building game development tools, or wanting to provide an abstract way of instancing objects in your app without having to hard-code all of the different types, you could easily do so. You create a new instance based on what object name is provided (such as a object called "Hydrant" and your user selecting a Hydrant in the game, thus requesting an object called Hydrant), then you verify if it implements one or more protocols such as "Interactable" or "destructible" and then invoke the methods or access the properties that are guaranteed to exist.

The Factory

The Factory will be the heart and soul of this example. It's responsibility is to create the object instances, invoke their methods or access their properties for your models and/or view controllers. Assuming you have created a fresh project, we will create new Objective-C class called Factory, which inherits from NSObject. We will provide it with the following public methods in the header.

@interface OFactory : NSObject

- (id)createObject:(NSString *)objectName;
- (void)invokeMethod:(NSString *)methodName onObject:(id)object;

@end

The first method we will implement is the createObject: method. This method will take a NSString object that hopefully has a valid object name. This could be done by creating another method called objectIsValidForCreation:(NSString *)string; that verifies that the object passed as a string has a matching class with the same name. We can look at that in another post. For now, we assume that the user always passes a string with a objectName that matches an existing class. If not, nil will be returned.

Next we have the invokeMethod: onObject: method which will take a string containing a method name and invoke it on the object supplied as an argument.

Let's implement the createObject: method first in our implememtation file.

#import "Factory.h"
@import ObjectiveC.runtime;

@implementation Factory

- (id)createObject:(NSString *)objectName {
    id objectInstance = [[NSClassFromString(objectName) alloc] init];
    return objectInstance;
}

@end

This is a pretty easy method to implement. We just need to make sure that the ObjectiveC.runtime api is imported. Using the Objective-C runtime we can instance a class by providing it with a NSString object containing the name of the class. The NSClassFromString method does just that. It returns a class, which we then alloc and init. You now can invoke this method like such:

Factory *factory = [[Factory alloc] init];
id testObject = [factory createObject:@"NSDate"];

if ([testObject isKindOfClass:[NSDate class]]) {
    NSLog(@"%@", [testObject description]);
}

When you run that code, you should see the current date printed to the debugger. Pretty cool right?

Next, let's build a simple class called "User" that implements two properties and a initializer. The header looks like this.

@interface User : NSObject
@property (strong, nonatomic) NSString *name;
@property (nonatomic) int age;

- (id)initWithName:(NSString *)name andAge:(int)age;
@end

The method we will implement will be done in the .m implementation file. Let's implement the description method, which is a method belonging to the super class NSObject

@implementation OFTestObject

- (id)init {
    self = [super init];
    if (self) {
        self.name = @"Billy";
        self.age = 33;
    }
    return self;
}

- (id)initWithName:(NSString *)name andAge:(int)age {
    self = [super init];
    if (self) {
        self.name = name;
        self.age = age;
    }
    return self;
}

- (NSString *)description {
    return [NSString stringWithFormat:@"The user name is %@ and is %d years old", self.name, self.age];
}
@end

Done. We can use this class to test our next feature, method invoking.

I actually already showed how to do this in my Monitoring Property Changes at Runtime post. There are several changes that I made however to make the implementation more durable in a dynamic environment. This time around it provides support for a unlimited number of arguments that can be passed into the invokeMethod and it returns the same return value that the method you invoked returned.

- (id)invokeMethod:(NSString *)methodName onObject:(id)object withParameter:(id)param, ... {
    if (methodName || object) {
        if ([object respondsToSelector:NSSelectorFromString(methodName)]) {
            // Invoke the getter of the property
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[object class] instanceMethodSignatureForSelector:NSSelectorFromString(methodName)]];
            if (invocation) {
                invocation.target = object;
                invocation.selector = NSSelectorFromString(methodName);

                // Pass the name of the property as the valueForKey: key argument.
                @try {
                    if (param) {
                        // first value is not part of the argument list, so it has to be handled separately.
                        [invocation setArgument:&param atIndex:2];

                        id eachObject;
                        va_list argumentList;

                        va_start(argumentList, param);
                        int index = 3; // Next in the index.
                        while ((eachObject = va_arg(argumentList, id))) {
                            [invocation setArgument:&eachObject atIndex:index];
                            index++;
                        }
                        va_end(argumentList);
                    }

                    id returnValue;
                    [invocation invoke];
                    [invocation getReturnValue:&returnValue];
                    return returnValue;
                }
                @catch (NSException *exception) {
                    NSLog(@"Failed to invoke the method");
                }
            } else {
                NSLog(@"ERROR: Failed to locate the method for @selector:%@", methodName);
            }
        }
    }
    return NULL;
}

Now if you want to test out this code, you can try it with the following code in a view controller some place.

self.factory = [[Factory alloc] init];
id testObject = [self.factory createObject:@"User"];

// user property can be a id, since the class is determined at run time.
self.user = [self.factory invokeMethod:@"description" onObject:testObject withParameter:nil];

The above code will instance a new factory, ask the factory to instance a User class and then invoke that User class method description. If the user class does not exist, nil is returned. In the event that the method does not exist or fails, nil is returned as well. The factory does all of the retrospection required to ensure the app does not crash.