Wednesday, November 24, 2010

Making NSInvocation even more useful (and painless)

I recently discovered the usefulness of NSInvocation to decouple different parts of my code. Basically, one of my objects wants an action performed on it by another, but I don't always want to define an interface or expose one class to another.

NSInvocation wraps everything that's needed for a selector invocation: the target object, the selector, the arguments and even the result. However, there's a lot of boilerplate code required to set one up, which is one of the reasons it eluded me for so long:
SEL selector = @selector(mySelector:);
NSString *argument = @"Hello World!";
NSInvocation *invocation = [NSInvocation 
  invocationWithMethodSignature:
  [self methodSignatureForSelector:selector]];
[invocation setTarget:self];
[invocation setSelector:selector];
[invocation setArgument:&argument atIndex:2];
[invocation retainArguments];
Note that the first argument is at index 2, since self and _cmd are passed implicitly. I also make the invocation retain its argument to make it self-contained.

Now you can pass the invocation to any other object who only needs to invoke it:
[invocation invoke];
Having found a life-saving use for this construct, I set out to make life easier for myself:
@implementation NSInvocation (VikramsOneLineConstructors)
+ (NSInvocation *)invocationWithTarget:(id)target selector:(SEL)selector 
  arguments:(NSArray *)arguments {
  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:
    [target methodSignatureForSelector:selector]];
  [invocation setTarget:target];
  [invocation setSelector:selector];
  for (NSInteger i = 0; i < [arguments count]; i++) {
    id argument = [arguments objectAtIndex:i];
    [invocation setArgument:&argument atIndex:2+i];
  }
  [invocation retainArguments];
  return invocation;
}
+ (NSInvocation *)invocationWithTarget:(id)target selector:(SEL)selector argument:(id)argument {
  return [NSInvocation invocationWithTarget:target selector:selector arguments:[NSArray arrayWithObject:argument]];
}
@end
I'm now a carefree convert to invocations, as I can simply do this:
button.action = [NSInvocation invocationWithTarget:self
  selector:@selector(browseURL:) argument:url];