UIButton block equivalent to addTarget:action:forControlEvents: method?
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:开发者_如何学C@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!"); }];
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
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.
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
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")
}
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'
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
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.
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)
}
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];
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:
- If you declare the block variable
static
, you can omitobjc_setAssociatedObject
; however, you cannot use any non-compile constants (such assome_local_variable
). - You can substitute
void(^)(void)
withdispatch_block_t
.
精彩评论