65

I looked around, but couldn't find this on the internet, nor anywhere in the Apple docs, so I'm guessing it doesn't exist.

But is there a iOS4 blocks equivalent API to:

[button addTarget:self action:@selector(tappy:) forControlEvents:UIControlEventTouchUpInside];

I suppose this could be implemented using a category, but would rather not write this myself due to extreme laziness :)

Something like this would be awesome:

[button handleControlEvent:UIControlEventTouchUpInside withBlock:^ { NSLog(@"I was tapped!"); }];
Ben Scheirman
  • 40,531
  • 21
  • 102
  • 137
  • I don't know why they didn't add built-in support for blocks instead of target / action at some point but maybe it's just because it is so easy to roll a little helper class that holds a block that nobody really actually needs such an extension of the standard API... – yeoman Nov 06 '16 at 13:59

10 Answers10

55

I just implemented this. It work's like a charm!

And it wasn't even hard.

typedef void (^ActionBlock)();

@interface UIBlockButton : UIButton {
    ActionBlock _actionBlock;
}

-(void) handleControlEvent:(UIControlEvents)event
                 withBlock:(ActionBlock) action;
@end

@implementation UIBlockButton

-(void) handleControlEvent:(UIControlEvents)event
                 withBlock:(ActionBlock) action
{
    _actionBlock = action;
    [self addTarget:self action:@selector(callActionBlock:) forControlEvents:event];
}

-(void) callActionBlock:(id)sender{
    _actionBlock();
}
@end
mylogon
  • 2,772
  • 2
  • 28
  • 42
Martin Reichl
  • 932
  • 9
  • 13
  • 4
    Is there any way to do this to existing buttons via a category? I know the trouble is having an instance variable you can save the block for.. – Ben Scheirman Oct 20 '10 at 17:26
  • 26
    Subclassing UIButton is an anti-pattern that is open for problems in the future. It's a class cluster and has only one way to correctly initialize it - through a class method that will never return one of your subclass instance. It might work now, it may fail at *any* time in the future. – Eiko Sep 21 '11 at 20:10
  • 10
    Eiko, excuse me. What kind of anti-pattern are you talking about? 'CallSuper'? As far as I know, 'Subclassing UIButton' isn't anti-pattern. – Sound Blaster Jan 18 '12 at 09:25
  • 10
    Subclassing UIButton is an anti-pattern because to do it safely you would need to override EVERYTHING in it's interface... which is crazy. I've run into crazy things happening when working with subclasses of UIBarButtonItems. Generally speaking; don't subclass class clusters. How to do it safely: (short) http://www.mikeash.com/pyblog/friday-qa-2010-03-12-subclassing-class-clusters.html and again: (longer and more detailed) http://cocoawithlove.com/2008/12/ordereddictionary-subclassing-cocoa.html – Gabe Jun 18 '12 at 15:54
  • If I use ARC, how to change this? – jeswang Jul 12 '12 at 07:15
  • @Gabe So then what is the 'correct' way to implement something like this? – Johnny Dec 06 '12 at 08:40
  • @Johnny This code does different things on different versions of iOS because it's a subclass(try adding a button of this class to a XIB in iOS4 and iOS5 and toggling 'Custom' and 'Rounded'). The 'correct' way to implement this is with a Category, There are several implementations of that here(including mine). – Gabe Feb 11 '13 at 15:16
  • 10
    @Gabe `UIBarButtonItem`s aren't buttons but *items*. `UIButton` can safely be subclassed, Apple even mentions it in the documentation. – Christian Schnorr May 26 '15 at 13:01
  • @Christian Oops, you are correct that these aren't class clusters... There are several problems similar to problems you have with overriding class-clusters with overriding UIButtons: The designated initializer changed functionality in iOS7 and you can't override the new UIButtonTypeSystem. http://www.splinter.com.au/2014/09/04/subclass-uibutton/ <- goes over the issues in more detail – Gabe May 28 '15 at 20:24
24

There is a library of blocks additions to the common Foundation/UI classes: BlocksKit. Here is the documentation.

It does not subclass UIButton, but adds UIControl category:

[button addEventHandler:^(id sender) {
    //do something
} forControlEvents:UIControlEventTouchUpInside];

There is also blocks/functional additions to collections (map, filter, etc), views-related stuff and more.

NOTE: it does not play well with Swift.

Cfr
  • 5,092
  • 1
  • 33
  • 50
  • Requires manual download and install of extra libs, on top of BlocksKit itself. Nice idea, but ... far too much effort for such a simple feature :(. – Adam Nov 02 '13 at 14:03
  • 2
    The function in BlocksKit 2.x is now `bk_addEventHandler`. See [this example](https://gist.github.com/jazzsasori/9051537) as a guide for using `addEventHandler` in BlocksKit 1.x/2.x – mopsled Aug 22 '15 at 07:20
  • Always better to use a (well maintained and popular) library, because you'll get updates in the future. And it works well with Swift. – Thomas Wana Mar 31 '16 at 20:45
22

Here's a working category implementation. In it's current form, this should only be used in DEBUG. I use this category in conjunction with a function (included below) to test various bits of code when user interaction and timing are important. Again this is only for development/debug purposes and shouldn't be considered for production, hence the #ifdef DEBUG ;)

#ifdef DEBUG

#import <objc/runtime.h>

static char UIButtonBlockKey;

@interface UIButton (UIBlockButton)

- (void)handleControlEvent:(UIControlEvents)event withBlock:(ActionBlock)block;
- (void)callActionBlock:(id)sender;

@end


@implementation UIButton (UIBlockButton)

- (void)handleControlEvent:(UIControlEvents)event withBlock:(ActionBlock)block {
    objc_setAssociatedObject(self, &UIButtonBlockKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self addTarget:self action:@selector(callActionBlock:) forControlEvents:event];
}


- (void)callActionBlock:(id)sender {
    ActionBlock block = (ActionBlock)objc_getAssociatedObject(self, &UIButtonBlockKey);
    if (block) {
        block();
    }
}

@end


void DSAddGlobalButton(NSString *title, ActionBlock block) {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button setTitle:title forState:UIControlStateNormal];
    [button handleControlEvent:UIControlEventTouchUpInside withBlock:block];
    [button sizeToFit];
    [button setFrame:(CGRect){{100.0f, 100.0f}, [button frame].size}];

    UIView *firstView = [[[[UIApplication sharedApplication] keyWindow] subviews] objectAtIndex:0];
    [firstView addSubview:button];
}


#endif
Shayne Sweeney
  • 561
  • 4
  • 8
12

Swift 4

class ClosureSleeve {
    let closure: () -> ()

    init(attachTo: AnyObject, closure: @escaping () -> ()) {
        self.closure = closure
        objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
    }

    @objc func invoke() {
        closure()
    }
}

extension UIControl {
    func addAction(for controlEvents: UIControlEvents = .primaryActionTriggered, action: @escaping () -> ()) {
        let sleeve = ClosureSleeve(attachTo: self, closure: action)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
    }
}

Example Usage:

button.addAction {
    print("button pressed")
}
Robin Daugherty
  • 7,115
  • 4
  • 45
  • 59
levin varghese
  • 810
  • 12
  • 12
7

I created a library to do just this!

It supports UIControl (UIButton), UIBarButtonItem, and UIGestureRecognizer. It is also supported using CocoaPods.

https://github.com/lavoy/ALActionBlocks

// Assuming you have a UIButton named 'button'
[button handleControlEvents:UIControlEventTouchUpInside withBlock:^(id weakControl) {
    NSLog(@"button pressed");
}];

Install

pod 'ALActionBlocks'
dimohamdy
  • 2,917
  • 30
  • 28
lavoy
  • 1,876
  • 3
  • 21
  • 36
5

I WROTE THIS LONG AGO AND IT'S NOT THE WAY TO SOLVE THIS PROBLEM!!! Subclassing UIButton creates a minefield that just isn't worth it. Use Shayne Sweeney's Category(I just updated his answer with a bunch of tweaks to make his example production ready... hopefully they get approved quickly).

-----ORIG POST-----

The code posted by Martin should work if you are only assigning the UIControlEventTouchUpInside... but there are a couple problems:

  • You will leak blocks with the code posted if you call handleControlEvent: more than once.
  • If you assign more than one type of event, it will fire the last block for all events

In my code I'm relying on Blocks being treated as object-c objects, which only works on iOS4+(not 3.2). It works well for me when I want to do something special for button states(i.e. animations). You can just use the clickedButton block for handling normal clicks.

#import <UIKit/UIKit.h>

@interface ButtWithBlockActions : UIButton {
  void (^downBlock_)(void);
  void (^upBlock_)(void);
  void (^clickedBlock_)(void);
}

@property(nonatomic,retain) void (^downBlock)(void);
@property(nonatomic,retain) void (^upBlock)(void);
@property(nonatomic,retain) void (^clickedBlock)(void);

@end



#import "ButtWithBlockActions.h"

@implementation ButtWithBlockActions

- (void)dealloc {
  [downBlock_ release];
  [upBlock_ release];
  [clickedBlock_ release];
  [super dealloc];
}


- (void (^)(void))downBlock { return downBlock_; }
- (void) fireDownBlock { downBlock_(); }
- (void) setDownBlock:(void (^)(void))block {
  if(downBlock_) {
    [self removeTarget:self action:@selector(fireDownBlock) forControlEvents:UIControlEventTouchDown];
    [self removeTarget:self action:@selector(fireDownBlock) forControlEvents:UIControlEventTouchDragEnter];
    [downBlock_ release];
  }
  downBlock_ = [block copy];
  if(downBlock_) {
    [self addTarget:self action:@selector(fireDownBlock) forControlEvents:UIControlEventTouchDown];
    [self addTarget:self action:@selector(fireDownBlock) forControlEvents:UIControlEventTouchDragEnter];
  }
}


- (void (^)(void))upBlock { return upBlock_; }
- (void) fireUpBlock { upBlock_(); }
- (void) setUpBlock:(void (^)(void))block {
  if(upBlock_) {
    [self removeTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchUpInside];
    [self removeTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchUpOutside];
    [self removeTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchDragOutside];
    [self removeTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchCancel];
    [upBlock_ release];
  }
  upBlock_ = [block copy];
  if(upBlock_) {
    [self addTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchUpInside];
    [self addTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchUpOutside];
    [self addTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchDragOutside];
    [self addTarget:self action:@selector(fireUpBlock) forControlEvents:UIControlEventTouchCancel];
  }
}


- (void (^)(void))clickedBlock { return clickedBlock_; }
- (void) fireClickedBlock { clickedBlock_(); }
- (void) setClickedBlock:(void (^)(void))block {
  if(clickedBlock_) {
    [self removeTarget:self action:@selector(fireClickedBlock) forControlEvents:UIControlEventTouchUpInside];
    [clickedBlock_ release];
  }
  clickedBlock_ = [block copy];
  if(clickedBlock_) {
    [self addTarget:self action:@selector(fireClickedBlock) forControlEvents:UIControlEventTouchUpInside];
  }
}

@end
Gabe
  • 2,279
  • 1
  • 23
  • 22
1

There is REKit which brings out Blocks latent ability. It gives you ability to add/override method to a instance using Block.

With REKit, you can dynamically make a target - which responds to buttonAction - like below:

id target;
target = [[NSObject alloc] init];
[target respondsToSelector:@selector(buttonAction) withKey:nil usingBlock:^(id receiver) {
    // Do something…
}];
[button addTarget:target action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];

You don't need to make a subclass nor a category.

In addition to target/action paradigm, you can use REKit for delegation pattern.

Kazki
  • 61
  • 3
1

Swift extension / category based implementation that I whipped up. Using OBJC associated objects is not an anti-pattern. :P

import UIKit

// MARK: UIControl Block based actions
typealias ActionBlock = (UIControl) -> ()

class UIButtonActionDelegate : NSObject {
    let actionBlock : ActionBlock
    init(actionBlock: ActionBlock) {
        self.actionBlock = actionBlock
    }
    func triggerBlock(control : UIControl) {
        actionBlock(control)
    }
}

private var actionHandlersKey: UInt8 = 0
extension UIControl {
    var actionHandlers: NSMutableArray { // cat is *effectively* a stored property
        get {
            return associatedObject(self, key: &actionHandlersKey, initialiser: { () -> NSMutableArray in
                return NSMutableArray()
            })
        }
        set { associateObject(self, key: &actionHandlersKey, value: newValue) }
    }

    func addBlockForEvents(events: UIControlEvents, block: ActionBlock) {
        let actionDelegate = UIButtonActionDelegate(actionBlock: block)
        actionHandlers.addObject(actionDelegate) // So it gets retained
        addTarget(actionDelegate, action: #selector(UIButtonActionDelegate.triggerBlock(_:)), forControlEvents: events)
    }
}

// MARK: Associated Object wrapper

func associatedObject<ValueType: AnyObject>(
    base: AnyObject,
    key: UnsafePointer<UInt8>,
    initialiser: () -> ValueType)
    -> ValueType {
        if let associated = objc_getAssociatedObject(base, key)
            as? ValueType { return associated }
        let associated = initialiser()
        objc_setAssociatedObject(base, key, associated,
                                 .OBJC_ASSOCIATION_RETAIN)
        return associated
}

func associateObject<ValueType: AnyObject>(
    base: AnyObject,
    key: UnsafePointer<UInt8>,
    value: ValueType) {
    objc_setAssociatedObject(base, key, value,
                             .OBJC_ASSOCIATION_RETAIN)
}
BadPirate
  • 25,802
  • 10
  • 92
  • 123
1

I find it easy and versatile to use a tiny helper class:

@interface Handler : NSObject

@end

@implementation Handler {
    void (^block)(id);
}

+ (Handler *)create:(void (^)(id))block {
    Handler *result = [[Handler alloc] init];

    result->block = block;

    return result;
}

- (void)call:(id)sender {
    block(sender);
}

@end

and use it like this:

Handler *handler = [Handler create:^(id sender) {
    // ... handle the event, using local state captured by the block ...
}];

// store the handler because the target is not retained in addTarget
[handlers addObject:handler];

[button addTarget:handler action:@selector(call:) forControlEvents:UIControlEventTouchUpInside];
yeoman
  • 1,671
  • 12
  • 15
0

A simpler solution (one without extensions, etc.):

void (^eventHandlerBlock)(void) = ^{
    printf("\nHandling event for button %lu\n", some_local_variable);
};
objc_setAssociatedObject(button, @selector(invoke), eventHandlerBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[button addTarget:eventHandlerBlock action:@selector(invoke) forControlEvents:UIControlEventAllEvents];

A couple of things to keep in mind when substituting selectors for blocks:

  1. If you declare the block variable static, you can omit objc_setAssociatedObject; however, you cannot use any non-compile constants (such as some_local_variable).
  2. You can substitute void(^)(void) with dispatch_block_t.
James Bush
  • 1,485
  • 14
  • 19