我可以通过Objective-C将块作为@selector传递吗?


90

是否可以通过Objective-C块作为@selector参数中的UIButton?即,有什么方法可以使以下工作正常进行?

    [closeOverlayButton addTarget:self 
                           action:^ {[anotherIvarLocalToThisMethod removeFromSuperview];} 
                 forControlEvents:UIControlEventTouchUpInside];

谢谢

Answers:


69

是的,但是您必须使用类别。

就像是:

@interface UIControl (DDBlockActions)

- (void) addEventHandler:(void(^)(void))handler 
        forControlEvents:(UIControlEvents)controlEvents;

@end

实现会有些棘手:

#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

一些解释:

  1. 我们正在使用一个名为的自定义“仅内部”类DDBlockActionWrapper。这是一个简单的类,具有一个块属性(我们要调用的块)和一个简单地调用该块的方法。
  2. UIControl类别简单地实例化这些包装中的一个,将其提供给被调用的块,然后告诉本身使用该包装件和其invokeBlock:作为目标和动作方法(正常)。
  3. UIControl类别使用关联的对象来存储的数组DDBlockActionWrappers,因为UIControl它不保留其目标。该数组用于确保在应该调用它们时存在这些块。
  4. 我们必须确保在DDBlockActionWrappers销毁对象时将其清理干净,因此我们正在进行令人讨厌的修改,即-[UIControl dealloc]使用一个新对象删除关联的对象,然后调用原始dealloc代码。棘手,棘手。 实际上,关联对象在释放期间会自动清除

最后,此代码是在浏览器中键入的,尚未编译。可能有些问题。你的旅费可能会改变。


4
请注意,您现在可以使用objc_implementationWithBlock()class_addMethod()以比使用关联对象稍微有效的方式解决此问题(这意味着哈希查找不如方法查找有效)。可能不相关的性能差异,但这是另一种选择。
bbum 2011年

@bbum是什么意思imp_implementationWithBlock
vikingosegundo

是的-那一个。它曾经被命名objc_implementationWithBlock()。:)
bbum 2011年

在custom UITableViewCell的按钮中使用此按钮将导致所需的target-action重复,因为每个新目标都是一个新实例,而先前的目标不会因相同事件而被清除。您必须首先清理目标 for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
尤金(Eugene)2013年

我认为使上面的代码更清楚的一件事是知道UIControl可以接受许多target:action对。因此,需要创建一个可变数组来存储所有这些对
大约

41

块是对象。将您的代码块作为target参数传递,并@selector(invoke)作为action参数传递,如下所示:

id block = [^{NSLog(@"Hello, world");} copy];// Don't forget to -release.

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];

那很有意思。我将看今晚是否可以做类似的事情。可能会提出一个新问题。
2011年

31
这是“巧合”。它依赖于私有API;invokeBlock对象上的方法不是公开的,因此不打算以这种方式使用。
bbum 2011年

1
Bum:你是对的。我曾以为-invoke是公开的,但是我一直想更新我的答案并提交错误。
lemnar 2011年

1
看来这是一个很棒的解决方案,但我想知道苹果使用私有API是否可以接受它。
Brian

1
通过nil而不是时有效@selector(invoke)
k06a

17

不,选择器和块在Objective-C中不是兼容类型(实际上,它们是完全不同的东西)。您必须编写自己的方法,然后传递其选择器。


11
特别是,选择器不是您要执行的东西。它是您发送给对象的消息的名称(或者在这种情况下,有另一个对象发送给第三个对象:您告诉控件将[选择器转到此处]消息发送给目标)。块,而另一方面,你的东西执行:您直接致电块独立的对象。
Peter Hosey

7

是否可以在UIButton中为@selector参数传递Objective-C块?

接受所有已经提供的答案,答案是肯定的,但是需要一些工作来设置某些类别。

我建议使用NSInvocation,因为您可以做很多事情,例如使用计时器,将其存储为对象并调用...等...

这是我所做的,但请注意我正在使用ARC。

首先是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

接下来是要存储在块中的NSInvocation上的类别:

。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

使用方法如下:

NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
            NSLog(@"TEST");
        }];
[invocation invoke];

您可以使用调用和标准的Objective-C方法做很多事情。例如,您可以使用NSInvocationOperation(initWithInvocation :),NSTimer(scheduledTimerWithTimeInterval:invocation:repeates :)

关键是将您的代码块转换为NSInvocation更加通用,可以这样使用:

NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
                NSLog(@"My Block code here");
            }];
[button addTarget:invocation
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];

同样,这只是一个建议。


还有一点,这里调用的是一个公共方法。developer.apple.com/library/mac/#documentation/Cocoa/Reference/...
阿尔文

5

不幸的是,事情并非如此简单。

从理论上讲,可以定义一个函数,该函数动态地将方法添加到的类中target,让该方法执行块的内容,并根据action参数的需要返回选择器。此功能可以使用MABlockClosure所使用的技术,在iOS上,该技术取决于libffi的自定义实现,而该实现仍处于实验阶段。

您最好将动作作为一种方法来实现。


4

Github上的BlocksKit库(也可以作为CocoaPod获得)具有内置的此功能。

看一下UIControl + BlocksKit.h的头文件。他们已经实现了Dave DeLong的想法,所以您不必这样做。一些文档在这里


1

有人会告诉我为什么这是错误的,也许是运气,也许不是,所以我会学点东西,或者会有所帮助。

我只是把这些放在一起。这真的很基础,只是带有一些转换的薄包装。提示一下,它假定您正在调用的块具有正确的签名以匹配您使用的选择器(即参数和类型的数量)。

//
//  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

//
//  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

确实没有发生任何神奇的事情。void *在调用该方法之前,仅将大量转换和类型转换转换为可用的块签名。显然(就像performSelector:及其关联方法一样,输入的可能组合是有限的,但是如果您修改代码,则可以扩展。

像这样使用:

BlockInvocation *invocation = [BlockInvocation invocationWithBlock:^(NSString *str) {
    NSLog(@"Block was invoked with str = %@", str);
}];
[invocation performWithObject:@"Test"];

它输出:

2011-01-03 16:11:16.020 BlockInvocation [37096:a0f]使用str = Test调用了块

在目标动作方案中使用时,您只需要执行以下操作:

BlockInvocation *invocation = [[BlockInvocation alloc] initWithBlock:^(id sender) {
  NSLog(@"Button with title %@ was clicked", [(NSButton *)sender title]);
}];
[myButton setTarget:invocation];
[myButton setAction:@selector(performWithObject:)];

由于未保留目标操作系统中的目标,因此您需要确保调用对象的寿命与控件本身一样长。

我很想听到比我更专业的人的消息。


您在该目标操作方案中发生了内存泄漏,因为invocation它从未发布
2011年

1

我需要有一个与UITableViewCell中的UIButton关联的动作。我想避免使用标签来跟踪每个不同单元格中的每个按钮。我认为实现这一目标的最直接方法是将一个“动作”块与该按钮相关联,如下所示:

[cell.trashButton addTarget:self withActionBlock:^{
        NSLog(@"Will remove item #%d from cart!", indexPath.row);
        ...
    }
    forControlEvent:UIControlEventTouchUpInside];

由于@bbum提到了imp_implementationWithBlockclass_addMethod,我的实现更加简化了(尽管未经广泛测试):

#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

拥有NSBlockOperation(iOS SDK +5)不可行。该代码使用ARC,是我正在测试的App的简化版(似乎可以正常工作,至少显然地,不确定我是否在泄漏内存)。

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];
}

当然,我不确定这对于实际使用有多好。您需要保持对NSBlockOperation的引用,否则我认为ARC将杀死它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.