Forcing a method to run on the main thread
You know how many times, as an iOS developer, you want to ensure some method or delegate call occurs on the main thread? We do this dance countless times, and while it’s straight-forward enough, it can be a burden sometimes.
Here’s an easy way to keep your thread access safe, without having to write a lot of code in the process.
In my code, I usually create a private delegate wrapper method for my delegate selectors, so that I don’t sprinkle logic all throughout my classes; there’s a single point of code where my delegates are invoked, meaning I can’t screw it up. These wrapper methods look something like this:
- (void)delegate_willShowViewController:(UIViewController*)controller {
if ([self.delegate respondsToSelector:@selector(pagedCellController:willShowViewController:)]) {
[self.delegate pagedCellController:self willShowViewController:controller];
}
}
This way I have a tidy way of handling defaults in the event the delegate doesn’t adopt the selector, and my code is much simpler. However, there are many UI-related calls that should be performed on the main thread only. I have a lot of boilerplate code that handles this, but I wanted an easy way to automate the process.
Without further ado, here’s a C function and C macro that simplifies kicking off any local selector on the main thread, only if the call isn’t already being made on the main thread.
#import <Foundation/Foundation.h>
extern NSInvocation * DNInvocationForCall(id object, SEL selector, ...);
#define FORCE_MAIN_THREAD(...) if (![NSThread isMainThread]) {
NSInvocation *inv = DNInvocationForCall(self, _cmd, ##__VA_ARGS__);
dispatch_async(dispatch_get_main_queue(), ^{
[inv invoke];
});
return;
}
#import "Defines.h"
NSInvocation * DNInvocationForCall(id object, SEL selector, ...) {
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[NSMethodSignature methodSignatureForSelector:selector]];
[inv setTarget:object];
[inv setSelector:selector];
va_list arglist;
va_start(arglist, selector);
NSInteger nextArgIndex = 2;
NSObject *arg = nil;
do {
arg = va_arg(arglist, NSObject*);
if (arg && arg != [NSNull null]) {
[inv setArgument:&arg atIndex:nextArgIndex];
}
nextArgIndex++;
} while (arg != nil);
va_end(arglist);
[inv retainArguments];
return inv;
}
The function crafts an NSInvocation object based on the object, selector, and a valist of arguments supplied to it. The macro wraps this, and only invokes it when not on the main thread. More importantly, the macro has the ability to return from the current method, halting execution on the current thread. Now, all I’d need to do is change my delegate wrapper to look like the following:
- (void)delegate_willShowViewController:(UIViewController*)controller {
FORCE_MAIN_THREAD(controller)
if ([self.delegate respondsToSelector:@selector(pagedCellController:willShowViewController:)]) {
[self.delegate pagedCellController:self willShowViewController:controller];
}
}
I hope this helps you.