No, selectors and blocks are not compatible types in Objective-C (in fact, they're very different things). You'll have to write your own method and pass its selector instead.
In theory, it would be possible to define a function that dynamically adds a method to the class of target, have that method execute the contents of a block, and return a selector as needed by the action argument. This function could use the technique used by MABlockClosure, which, in the case of iOS, depends on a custom implementation of libffi, which is still experimental.
You’re better off implementing the action as a method.
We're using a custom "internal only" class called DDBlockActionWrapper. This is a simple class that has a block property (the block we want to get invoked), and a method that simply invokes that block.
The UIControl category simply instantiates one of these wrappers, gives it the block to be invoked, and then tells itself to use that wrapper and its invokeBlock: method as the target and action (as normal).
The UIControl category uses an associated object to store an array of DDBlockActionWrappers, because UIControl does not retain its targets. This array is to ensure that the blocks exist when they're supposed to be invoked.
We have to ensure that the DDBlockActionWrappers get cleaned up when the object is destroyed, so we're doing a nasty hack of swizzling out -[UIControl dealloc] with a new one that removes the associated object, and then invokes the original dealloc code. Tricky, tricky. Actually, associated objects are cleaned up automatically during deallocation.
Finally, this code was typed in the browser and has not been compiled. There are probably some things wrong with it. Your mileage may vary.
Somebody is going to tell me why this is wrong, maybe, or with any luck, maybe not, so I'll either learn something, or I'll be helpful.
I just threw this together. It's really basic, just a thin-wrapper with a bit of casting. A word of warning, it assumes the block you're invoking has the correct signature to match the selector you use (i.e. number of arguments and types).
//
// BlockInvocation.h
// BlockInvocation
//
// Created by Chris Corbyn on 3/01/11.
// Copyright 2011 __MyCompanyName__. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface BlockInvocation : NSObject {
void *block;
}
-(id)initWithBlock:(void *)aBlock;
+(BlockInvocation *)invocationWithBlock:(void *)aBlock;
-(void)perform;
-(void)performWithObject:(id)anObject;
-(void)performWithObject:(id)anObject object:(id)anotherObject;
@end
There's really nothing magical going on. Just lots of downcasting to void * and typecasting to a usable block signature before invoking the method. Obviously (just like with performSelector: and associated method, the possible combinations of inputs are finite, but extendable if you modify the code.
Used like this:
BlockInvocation *invocation = [BlockInvocation invocationWithBlock:^(NSString *str) {
NSLog(@"Block was invoked with str = %@", str);
}];
[invocation performWithObject:@"Test"];
It outputs:
2011-01-03 16:11:16.020 BlockInvocation[37096:a0f] Block was invoked with str = Test
Used in a target-action scenario you just need to do something like this:
BlockInvocation *invocation = [[BlockInvocation alloc] initWithBlock:^(id sender) {
NSLog(@"Button with title %@ was clicked", [(NSButton *)sender title]);
}];
[myButton setTarget:invocation];
[myButton setAction:@selector(performWithObject:)];
Since the target in a target-action system is not retained, you will need to ensure the invocation object lives for as long as the control itself does.
I'm interested to hear anything from somebody more expert than me.
You can do a lot with the invocation and the standard Objective-C Methods. For example, you can use NSInvocationOperation (initWithInvocation:), NSTimer (scheduledTimerWithTimeInterval:invocation:repeates:)
The point is turning your block into an NSInvocation is more versatile and can be used as such:
Doesn't it work to have an NSBlockOperation (iOS SDK +5). This code uses ARC and it is a simplification of an App I am testing this with (seems to work, at least apparently, not sure if I am leaking memory).
I needed to have an action associated to a UIButton within a UITableViewCell. I wanted to avoid using tags to track down each button in every different cell. I thought the most direct way to achieve this was to associate a block "action" to the button like so:
My implementation is a bit more simplified, thanks to @bbum for mentioning imp_implementationWithBlock and class_addMethod, (although not extensively tested):
#import <objc/runtime.h>
@implementation UIButton (ActionBlock)
static int _methodIndex = 0;
- (void)addTarget:(id)target withActionBlock:(ActionBlock)block forControlEvent:(UIControlEvents)controlEvents{
if (!target) return;
NSString *methodName = [NSString stringWithFormat:@"_blockMethod%d", _methodIndex];
SEL newMethodName = sel_registerName([methodName UTF8String]);
IMP implementedMethod = imp_implementationWithBlock(block);
BOOL success = class_addMethod([target class], newMethodName, implementedMethod, "v@:");
NSLog(@"Method with block was %@", success ? @"added." : @"not added." );
if (!success) return;
[self addTarget:target action:newMethodName forControlEvents:controlEvents];
// On to the next method name...
++_methodIndex;
}
@end