The Problem
NSInvocation is a pretty useful class. It is Ovjective-C's version of a function object. It encapsulates a method call allowing you to pass it around, modify it, and call it later.The problem with NSInvocation is how hard it is to initialize. Here is the simplest NSInvocation construction I can think of:
SEL sel = @selector(myMethod);
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector:sel]];
[inv setTarget:self];
[inv setSelector:sel];
Clearly this is unwieldy at best, especially if you just want to make a method call. Even worse is trying to create an invocation when all you have is a protocol. I came across this problem when trying to implement the observer pattern in Objective-C. I wanted to call all observers in a loop, without having to copy and paste the loop code for every observer method I had. And I still wanted to support methods that take non-object parameters, which means
performSelector: was not an option.The Solution
Here is what I came up with. I added three new factory methods to the NSInvocation class with a category:@interface NSInvocation (Constructors) + (id)invocationWithTarget:(NSObject*)targetObject selector:(SEL)selector; + (id)invocationWithClass:(Class)targetClass selector:(SEL)selector; + (id)invocationWithProtocol:(Protocol*)targetProtocol selector:(SEL)selector; @endSo that initializing an invocation becomes a one-liner:
NSInvocation* inv = [NSInvocation invocationWithTarget:self
selector:@selector(myMethod)];
Even if all you have is a protocol:NSInvocation* inv = [NSInvocation invocationWithProtocol:@protocol(MyProtocol)
selector:@selector(myMethod)];
// ...Later:
[inv setTarget:someTarget];
[inv invoke];
The Code
The first factory method is easy to implement, it's what you would normally write to create an invocation:+ (id)invocationWithTarget:(NSObject*)targetObject selector:(SEL)selector {
NSMethodSignature* sig = [target methodSignatureForSelector:selector];
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig];
[inv setTarget:target];
[inv setSelector:selector];
return inv;
}
The next two factory methods are a bit more involved, they require using little-known Objective-C functions. First we'll need to use class_getInstanceMethod which returns a method description structure given a class and a selector. Then we call method_getDescription to get a method description stucture. From that we can construct an NSMethodSignature, which we use to construct the NSInvocation. The trick is using the signatureWithObjCTypes: factory method of the NSMethodSignature class with the types element of the description returned by method_getDescription:
+ (id)invocationWithClass:(Class)targetClass selector:(SEL)selector {
Method method = class_getInstanceMethod(targetClass, selector);
struct objc_method_description* desc = method_getDescription(method);
if (desc == NULL || desc->name == NULL)
return nil;
NSMethodSignature* sig = [NSMethodSignature signatureWithObjCTypes:desc->types];
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig];
[inv setSelector:selector];
return inv;
}
The protocol factory method is somewhat similar to the previous one but we only need one function call, protocol_getMethodDescription, which returns the description right away. The complication here is that you need to know if the method is static or not and if the method is required or optional. This seems kind of silly because the language should be able to determine that from the protocol definition. But anyway, lets assume the method will not be static. We first try assuming it is a required method and if that fails we try optional. Here is the code:
+ (id)invocationWithProtocol:(Protocol*)targetProtocol selector:(SEL)selector {
struct objc_method_description desc;
BOOL required = YES;
desc = protocol_getMethodDescription(targetProtocol, selector, required, YES);
if (desc.name == NULL) {
required = NO;
desc = protocol_getMethodDescription(targetProtocol, selector, required, YES);
}
if (desc.name == NULL)
return nil;
NSMethodSignature* sig = [NSMethodSignature signatureWithObjCTypes:desc.types];
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig];
[inv setSelector:selector];
return inv;
}
Conclusion
Creating an NSInvocation is so cumbersome as to be discouraging. At least once in the past, I have changed the design of a piece of code where I could use function objects just because I didn't want add all the ugly code needed to create them. But with the code here, in a single line, you can:- Create an NSInvocation from an object and a selector
- Create an NSInvocation from a class and a selector
- Create and NSInvocation from a protocol and a selector
Here is a different approach to the same problem by Matt Gallagher: Construct an NSInvocation for any message, just by sending. No support for constructing from a class or a protocol, though.
The source code for this article, including unit tests, is available at GitHub. To compile you will need Google Toolbox for Mac.
nice. arguments are nice to have too. :)
ReplyDeleteI created something similar at stackoverflow
http://stackoverflow.com/questions/5375127/what-is-the-easiest-way-to-make-an-nsinvocation-with-target-selector-and-argumen
I hate to wake up post that is nearly a year old but is there any chance of implementing this without the GTM? I don't want to add a huge framework to my app just for this one thing.
ReplyDeleteYes, GTM is used only for the unit tests.
ReplyDelete...actually just replace the GTMObjC2Runtime.h include with
ReplyDelete#import <objc/message.h>
#import <objc/runtime.h>