Thursday 27 January 2011

iOS: performSelectorOnMainThread: with two arguments

iOS's NSObject class provides a mechanism to run any selector on a different thread. The most often used variant runs the selector on the "main" (i.e. UI) thread, the snappily titled: performSelectorOnMainThread:withObject:waitUntilDone:. However, the limitation with this call is that you can only supply one argument.

Sometimes, one is not enough. There is a two argument non-thread version, here. So why not invent our own two-argument threaded version?

We can add this easily enough to the base NSObject class by opening a new category on it just for the purpose.

Here's how...

1. Define our API

We open a category on NSObject (note the category name in brackets)
@interface NSObject (PGPerformSelectorOnMainThreadWithTwoObjects)
- (void) performSelectorOnMainThread:(SEL)selector withObject:(id)arg1 withObject:(id)arg2 waitUntilDone:(BOOL)wait;
@end
Naturally, it follows the existing performSelector API signatures, but just adds our extra withObject: argument.

2. Implement it

Here's the secret sauce. Careful application of NSMethodSignature, and NSInvocation.
@implementation NSObject (PGPerformSelectorOnMainThreadWithTwoObjects)
- (void) performSelectorOnMainThread:(SEL)selector withObject:(id)arg1 withObject:(id)arg2 waitUntilDone:(BOOL)wait
{
NSMethodSignature *sig = [self methodSignatureForSelector:selector];
if (!sig) return;

NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig];
[invo setTarget:self];
[invo setSelector:selector];
[invo setArgument:&arg1 atIndex:2];
[invo setArgument:&arg2 atIndex:3];
[invo retainArguments];
[invo performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:wait];
}
@end

That's it.

Now you have your own simple way to invoke a call to selector on the main thread from wherever you are in your code. Handy.

2 comments:

xavi wise said...

neat trick, but a bit "overthinking"

just adding a small wrapper around the selector you wish to perform, to accept an NSArray of objects, and you are done to process as many objects as you need

performSelectorOnMainThread:@selector(mySelectorWithArgumentArray:)withObject:[NSArray arrayWithObjects:object1, object2, object3,...., objectN, nil] waitUntilDone:NO];

Pete Goodliffe said...

Not at all. There may be existing selectors you want to perform with that many parameters, that you cannot modify.

In that case, you'd have to write an NSArray shim for each one, perhaps in a category on the object (solution that scales badly).

Or use my extra perform method (solution that scales well).