Tuesday 29 April 2014

Dynamic Objective-C with NSInvocation

Although Objective-C may seem very much like Java or C++ to the untrained eye, there's actually a lot of trickery just waiting to be tapped. The dynamic nature of the language allows us to bundle up a method call as an object, and either customize it or reuse it with different targets and arguments.

First, let's create a method that we want to call:

- (NSString *) stringForDate: (NSDate *)date
usingFormatter: (NSDateFormatter *)formatter
{
  // yes, this doesn't do anything interesting.
  // just using it as a simple example

  return [formatter stringFromDate: date];
}


A brilliant masterpiece, isn't it? Now, outside of this method, let's prepare an NSInvocation object. First, we need an Objective-C selector and a matching NSMethodSignature:

// get an Objective-C selector variable for the method

SEL mySelector;
mySelector = @selector(stringForDate:usingFormatter:);

// create a signature from the selector

NSMethodSignature * sig = nil;
sig = [[self class] instanceMethodSignatureForSelector:mySelector];


Notice that we ask [self class] for the method signature. This is because the instanceMethodSignatureForSelector: class method is built into NSObject.

Now, make NSInvocation object itself:

// create an actual invocation object and set the target
// to self

NSInvocation * myInvocation = nil;
myInvocation = [NSInvocation invocationWithMethodSignature:sig];
[myInvocation setTarget:self];
[myInvocation setSelector:mySelector];


Pretty straightforward. We use the NSMethodSignature as input, and set the target to "self". You can, of course, send messages to other objects by setting a different target here.

Now, we need to add the arguments for the method:

// add first argument

NSDate * myDate = [NSDate date];
[myInvocation setArgument:&myDate atIndex:2];

// add second argument

NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[myInvocation setArgument:&dateFormatter atIndex:3];


Again, pretty simple. We create objects and simply assign them as arguments. We need to pass in the address of the objects, so we use the addressof operator (&).

One obvious question here is why do we add the first argument at index 2. The first argument should be index 0, right? Here's the answer:

Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; these values can be retrieved directly with the target and selector methods. Use indices 2 and greater for the arguments normally passed in a message.


Finally, we want to call the actual method and get a result:

// now activate the invocation and get the result

NSString * result = nil;
[myInvocation retainArguments];
[myInvocation invoke];
[myInvocation getReturnValue:&result];

NSLog(@"The result is: %@", result);


The result is written directly to the NSString variable. The output looks something like this:

The result is: 05/13/06

This may seem like an awkward way to send a message, but the point is that NSInvocation objects can be used to call arbitrary methods with arbitrary arguments which are determined at runtime, and the objects are mutable so you can send the same message to multiple targets easily, or just change arguments.

No comments:

Post a Comment