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

前端 未结 9 530
佛祖请我去吃肉
佛祖请我去吃肉 2020-11-30 17:04

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?

相关标签:
9条回答
  • 2020-11-30 17:46

    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.

    0 讨论(0)
  • 2020-11-30 17:51

    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.

    0 讨论(0)
  • 2020-11-30 17:52

    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.

    0 讨论(0)
  • 2020-11-30 17:57

    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.

    0 讨论(0)
  • 2020-11-30 17:57

    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
    
    0 讨论(0)
  • 2020-11-30 17:59

    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];
    
    0 讨论(0)
提交回复
热议问题