91

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

Bill Shiff
  • 1,429
  • 2
  • 14
  • 17

9 Answers9

70

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.

abbood
  • 23,101
  • 16
  • 132
  • 246
Dave DeLong
  • 242,470
  • 58
  • 448
  • 498
  • 4
    Note that you could now use `objc_implementationWithBlock()` and `class_addMethod()` to solve this problem in a slightly more efficient fashion than using associated objects (which imply a hash lookup that isn't as efficient as method lookup). Likely an irrelevant performance difference, but it is an alternative. – bbum Oct 25 '11 at 18:16
  • @bbum do you mean `imp_implementationWithBlock`? – vikingosegundo Oct 26 '11 at 01:16
  • Yeah -- that one. It was once named `objc_implementationWithBlock()`. :) – bbum Oct 26 '11 at 15:35
  • Using this for buttons in custom `UITableViewCell`'s will result in duplication of desired targets-actions since every new target is a new instance and the previous ones don't get cleaned up for the same events. You've gotta clean the targets first `for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];` – Eugene Mar 21 '13 at 21:17
  • I think one thing that makes the above code more clear is knowing that a UIControl can accept many target:action pairs.. hence the need to create a mutable array to store all those pairs – abbood May 28 '13 at 13:02
  • I'm surprised no one has mentioned the BlocksKit library, which has similar code baked in. Take a look at my answer for a link. – Nate Cook Jun 08 '13 at 00:32
  • @bbum what do you think of Matt Thompson's claim that doing the above is actually an *anti-pattern* in [this](http://nshipster.com/associated-objects/) article? – abbood Dec 13 '14 at 08:00
  • Amazing answer! helped me making a category for UIButton to handle events inline(Block). Thanks a lot. – Pratik Jamariya Sep 12 '16 at 12:56
41

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];
lemnar
  • 4,063
  • 1
  • 32
  • 44
  • That's interesting. I'll look to see if I can do something similar tonight. May start a new question. – Tad Donaghe Aug 24 '11 at 17:23
  • 31
    This "works" by coincidence. It relies on private API; the `invoke` method on Block objects is not public and not intended to be used in this fashion. – bbum Oct 25 '11 at 18:14
  • 1
    Bbum: You're right. I had thought -invoke was public, but I've been meaning to update my answer and file a bug. – lemnar Oct 25 '11 at 21:31
  • +1 interesting, but memory management (ownership of the block) seems to be a problem. I think this is a read-only block, which might explain why it doesn't crash. – Steven Kramer Nov 22 '11 at 10:59
  • Private API problem can be solved using pretty simple block-wrapper. Memory management can be solved using associatedObject runtime mechanism. – gavrix Nov 15 '12 at 00:13
  • 1
    it seems an awesome solution, but I am wondering whether it's acceptable by Apple as it uses a private API. – Brian Nov 19 '13 at 15:28
  • 1
    Works when passed `nil` instead of `@selector(invoke)`. – k06a Feb 24 '15 at 17:17
  • @Brian For those concerned to use this, `invoke` (and `invokeWithArgument:`) are PUBLIC methods! Wooo! https://developer.apple.com/documentation/foundation/nsinvocation/1437850-invoke?language=objc – Albert Renshaw Oct 21 '18 at 06:19
  • @AlbertRenshaw `NSInvocation` is a completely different class than blocks (which are internally types `NSStackBlock`, `NSMallocBlock`, etc.). The `invoke` method on blocks is still private. – cncool Nov 10 '18 at 09:09
18

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.

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
  • 11
    In particular, a selector is not something you execute; it is the name of the message you send to an object (or have another object send to a third object, as in this case: you're telling the control to send a [selector goes here] message to the target). A block, on the other hand, *is* something you execute: You call the block directly, independently of an object. – Peter Hosey Jan 03 '11 at 04:30
8

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.

Arvin
  • 2,516
  • 27
  • 30
  • One more thing, invoke here is a public method. https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSInvocation_Class/Reference/Reference.html#//apple_ref/occ/instm/NSInvocation/invoke – Arvin May 19 '12 at 18:33
5

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.

Quinn Taylor
  • 44,553
  • 16
  • 113
  • 131
4

The library BlocksKit 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.

Nate Cook
  • 8,395
  • 5
  • 46
  • 37
1

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
Don Miguel
  • 882
  • 9
  • 19
1

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.

d11wtq
  • 34,788
  • 19
  • 120
  • 195
0

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.

rufo
  • 5,158
  • 2
  • 36
  • 47