Can I pass a block as a @selector with Objective-C?

Objective CCocoa Touch

Objective C Problem Overview


Is it possible to pass an Objective-C block for the @selector argument in a UIButton? i.e., Is there any way to get the following to work?

	[closeOverlayButton addTarget:self 
						   action:^ {[anotherIvarLocalToThisMethod removeFromSuperview];} 
				 forControlEvents:UIControlEventTouchUpInside];

Thanks

Objective C Solutions


Solution 1 - Objective C

Yes, but you'd have to use a category.

Something like:

@interface UIControl (DDBlockActions)

- (void) addEventHandler:(void(^)(void))handler 
        forControlEvents:(UIControlEvents)controlEvents;

@end

The implementation would be a bit trickier:

#import <objc/runtime.h>

@interface DDBlockActionWrapper : NSObject
@property (nonatomic, copy) void (^blockAction)(void);
- (void) invokeBlock:(id)sender;
@end

@implementation DDBlockActionWrapper
@synthesize blockAction;
- (void) dealloc {
  [self setBlockAction:nil];
  [super dealloc];
}

- (void) invokeBlock:(id)sender {
  [self blockAction]();
}
@end

@implementation UIControl (DDBlockActions)

static const char * UIControlDDBlockActions = "unique";

- (void) addEventHandler:(void(^)(void))handler 
        forControlEvents:(UIControlEvents)controlEvents {

  NSMutableArray * blockActions = 
                 objc_getAssociatedObject(self, &UIControlDDBlockActions);

  if (blockActions == nil) {
    blockActions = [NSMutableArray array];
    objc_setAssociatedObject(self, &UIControlDDBlockActions, 
                                        blockActions, OBJC_ASSOCIATION_RETAIN);
  }

  DDBlockActionWrapper * target = [[DDBlockActionWrapper alloc] init];
  [target setBlockAction:handler];
  [blockActions addObject:target];

  [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
  [target release];

}

@end

Some explanation:

  1. 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.
  2. 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).
  3. 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.
  4. 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.

Solution 2 - Objective C

Blocks are objects. Pass your block as the target argument, with @selector(invoke) as the action argument, like this:

id block = [^{NSLog(@"Hello, world");} copy];// Don't forget to -release.

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];

Solution 3 - Objective C

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.

Solution 4 - Objective C

> Is it possible to pass an Objective-C block for the @selector argument in a UIButton?

Taking in all the already provided answers, the answer is Yes but a tiny bit of work is necessary to setup some categories.

I recommend using NSInvocation because you can do a lot with this such as with timers, stored as an object and invoked...etc...

Here is what I did, but note I am using ARC.

First is a simple category on NSObject:

.h

@interface NSObject (CategoryNSObject)

- (void) associateValue:(id)value withKey:(NSString *)aKey;
- (id) associatedValueForKey:(NSString *)aKey;

@end

.m

#import "Categories.h"
#import <objc/runtime.h>

@implementation NSObject (CategoryNSObject)

#pragma mark Associated Methods:

- (void) associateValue:(id)value withKey:(NSString *)aKey {
    
	objc_setAssociatedObject( self, (__bridge void *)aKey, value, OBJC_ASSOCIATION_RETAIN );
}

- (id) associatedValueForKey:(NSString *)aKey {
    
	return objc_getAssociatedObject( self, (__bridge void *)aKey );
}

@end

Next is a category on NSInvocation to store in a block:

.h

@interface NSInvocation (CategoryNSInvocation)

+ (NSInvocation *) invocationWithTarget:(id)aTarget block:(void (^)(id target))block;
+ (NSInvocation *) invocationWithSelector:(SEL)aSelector forTarget:(id)aTarget;
+ (NSInvocation *) invocationWithSelector:(SEL)aSelector andObject:(__autoreleasing id)anObject forTarget:(id)aTarget;

@end

.m

#import "Categories.h"

typedef void (^BlockInvocationBlock)(id target);

#pragma mark - Private Interface:

@interface BlockInvocation : NSObject
@property (readwrite, nonatomic, copy) BlockInvocationBlock block;
@end

#pragma mark - Invocation Container:

@implementation BlockInvocation

@synthesize block;

- (id) initWithBlock:(BlockInvocationBlock)aBlock {
    
    if ( (self = [super init]) ) {
        
        self.block = aBlock;
        
    } return self;
}

+ (BlockInvocation *) invocationWithBlock:(BlockInvocationBlock)aBlock {
    return [[self alloc] initWithBlock:aBlock];
}

- (void) performWithTarget:(id)aTarget {
    self.block(aTarget);
}

@end

#pragma mark Implementation:

@implementation NSInvocation (CategoryNSInvocation)

#pragma mark - Class Methods:

+ (NSInvocation *) invocationWithTarget:(id)aTarget block:(void (^)(id target))block {
    
    BlockInvocation *blockInvocation = [BlockInvocation invocationWithBlock:block];
    NSInvocation *invocation = [NSInvocation invocationWithSelector:@selector(performWithTarget:) andObject:aTarget forTarget:blockInvocation];
    [invocation associateValue:blockInvocation withKey:@"BlockInvocation"];
    return invocation;
}

+ (NSInvocation *) invocationWithSelector:(SEL)aSelector forTarget:(id)aTarget {
    
    NSMethodSignature   *aSignature  = [aTarget methodSignatureForSelector:aSelector];
    NSInvocation        *aInvocation = [NSInvocation invocationWithMethodSignature:aSignature];
    [aInvocation setTarget:aTarget];
    [aInvocation setSelector:aSelector];
    return aInvocation;
}

+ (NSInvocation *) invocationWithSelector:(SEL)aSelector andObject:(__autoreleasing id)anObject forTarget:(id)aTarget {
    
    NSInvocation *aInvocation = [NSInvocation invocationWithSelector:aSelector 
                                                           forTarget:aTarget];
    [aInvocation setArgument:&anObject atIndex:2];
    return aInvocation;
}

@end

Here is how to use it:

NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
            NSLog(@"TEST");
        }];
[invocation invoke];

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:

NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
                NSLog(@"My Block code here");
            }];
[button addTarget:invocation
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];

Again this is just one suggestion.

Solution 5 - Objective C

Not as simple as that, unfortunately.

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.

Solution 6 - Objective C

The library [BlocksKit][1] on Github (also available as a CocoaPod) has this feature built-in.

Take a look at the header file for UIControl+BlocksKit.h. They've implemented Dave DeLong's idea so you don't have to. Some documentation is [here][2].

[1]: https://github.com/pandamonia/BlocksKit "BlocksKit" [2]: http://pandamonia.github.io/BlocksKit/Documentation/Categories/UIControl+BlocksKit.html "here"

Solution 7 - Objective C

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

And

//
//  BlockInvocation.m
//  BlockInvocation
//
//  Created by Chris Corbyn on 3/01/11.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "BlockInvocation.h"


@implementation BlockInvocation

-(id)initWithBlock:(void *)aBlock {
	if (self = [self init]) {
		block = (void *)[(void (^)(void))aBlock copy];
	}
	
	return self;
}

+(BlockInvocation *)invocationWithBlock:(void *)aBlock {
	return [[[self alloc] initWithBlock:aBlock] autorelease];
}

-(void)perform {
	((void (^)(void))block)();
}

-(void)performWithObject:(id)anObject {
	((void (^)(id arg1))block)(anObject);
}

-(void)performWithObject:(id)anObject object:(id)anotherObject {
	((void (^)(id arg1, id arg2))block)(anObject, anotherObject);
}

-(void)dealloc {
	[(void (^)(void))block release];
	[super dealloc];
}

@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.

Solution 8 - Objective C

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:

[cell.trashButton addTarget:self withActionBlock:^{
        NSLog(@"Will remove item #%d from cart!", indexPath.row);
        ...
    }
    forControlEvent:UIControlEventTouchUpInside];

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

Solution 9 - Objective C

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).

NSBlockOperation *blockOp;
UIView *testView; 

-(void) createTestView{
    UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(0, 60, 1024, 688)];
    testView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:testView];            
    
    UIButton *btnBack = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [btnBack setFrame:CGRectMake(200, 200, 200, 70)];
    [btnBack.titleLabel setText:@"Back"];
    [testView addSubview:btnBack];

    blockOp = [NSBlockOperation blockOperationWithBlock:^{
        [testView removeFromSuperview];
    }];

    [btnBack addTarget:blockOp action:@selector(start) forControlEvents:UIControlEventTouchUpInside];
}

Of course, I am not sure how good this is for real usage. You need to keep a reference to the NSBlockOperation alive or I think that ARC will kill it.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBill ShiffView Question on Stackoverflow
Solution 1 - Objective CDave DeLongView Answer on Stackoverflow
Solution 2 - Objective ClemnarView Answer on Stackoverflow
Solution 3 - Objective CBoltClockView Answer on Stackoverflow
Solution 4 - Objective CArvinView Answer on Stackoverflow
Solution 5 - Objective Cuser557219View Answer on Stackoverflow
Solution 6 - Objective CNate CookView Answer on Stackoverflow
Solution 7 - Objective Cd11wtqView Answer on Stackoverflow
Solution 8 - Objective CDon MiguelView Answer on Stackoverflow
Solution 9 - Objective CrufoView Answer on Stackoverflow