UIButton block equivalent to addTarget:action:forControlEvents: method?

37,000

Solution 1

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

Solution 2

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.

Solution 3

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

Solution 4

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")
}

Solution 5

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'
Share:
37,000
smirkingman
Author by

smirkingman

I'm an iOS &amp; Rails developer working out of Houston, TX. I run [NSScreencast](http://nsscreencast.com), a site that delivers high quality screencasts for Swift developers. Other projects include: Combine Swift video course TonalTherapy GiggleTouch NSDateFormatter.com You can find me on twitter or on my blog.

Updated on November 18, 2021

Comments

  • smirkingman
    smirkingman over 2 years

    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!"); }];
    
  • smirkingman
    smirkingman over 13 years
    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..
  • Eiko
    Eiko over 12 years
    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.
  • Sound Blaster
    Sound Blaster over 12 years
    Eiko, excuse me. What kind of anti-pattern are you talking about? 'CallSuper'? As far as I know, 'Subclassing UIButton' isn't anti-pattern.
  • Gabe
    Gabe almost 12 years
    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) mikeash.com/pyblog/… and again: (longer and more detailed) cocoawithlove.com/2008/12/…
  • jeswang
    jeswang almost 12 years
    If I use ARC, how to change this?
  • Johnny
    Johnny over 11 years
    @Gabe So then what is the 'correct' way to implement something like this?
  • Gabe
    Gabe about 11 years
    @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).
  • Nate Cook
    Nate Cook almost 11 years
    What about this makes it unsuited for Production?
  • Adam
    Adam over 10 years
    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 :(.
  • Christian Schnorr
    Christian Schnorr almost 9 years
    @Gabe UIBarButtonItems aren't buttons but items. UIButton can safely be subclassed, Apple even mentions it in the documentation.
  • Gabe
    Gabe almost 9 years
    @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. splinter.com.au/2014/09/04/subclass-uibutton <- goes over the issues in more detail
  • mopsled
    mopsled over 8 years
    The function in BlocksKit 2.x is now bk_addEventHandler. See this example as a guide for using addEventHandler in BlocksKit 1.x/2.x
  • Thomas Wana
    Thomas Wana about 8 years
    Always better to use a (well maintained and popular) library, because you'll get updates in the future. And it works well with Swift.
  • Bijender Singh Shekhawat
    Bijender Singh Shekhawat over 5 years
    this is an answer for swift 4 and 4.2 also. thanx bro
  • Warpling
    Warpling about 5 years
    You could also use a UUID for the key rather than a random number: UUID().uuidString