我能用Objective-C将块作为@selector传递吗?

91

是否可以将Objective-C块作为@selector参数传递给UIButton?即,有没有办法使以下内容正常工作?

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

谢谢

9个回答

70

可以,但你需要使用分类。

就像这样:

@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,所以我们进行了一个肮脏的hack,通过一个新的方法替换掉-[UIControl dealloc],删除关联对象,然后调用原始的dealloc代码。挺棘手的。实际上,关联对象在释放时会自动清理

最后,这段代码是在浏览器中输入的,尚未编译。可能会有一些问题。结果因人而异。


4
请注意,您现在可以使用objc_implementationWithBlock()class_addMethod()来解决这个问题,比使用关联对象(它们意味着哈希查找不如方法查找高效)略微更有效率。虽然性能差异可能不大,但这是一种替代方案。 - bbum
是的 - 就是那个。它曾经被命名为 objc_implementationWithBlock()。 :) - bbum
在自定义的UITableViewCell中使用此代码来处理按钮会导致所需目标操作的重复,因为每个新目标都是一个新实例,并且以前的目标不会为相同的事件清除。您必须首先清除这些目标:for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents]; - Eugene
我认为让上述代码更清晰的一件事是知道UIControl可以接受许多目标:动作对,因此需要创建一个可变数组来存储所有这些对。 - abbood
我很惊讶没有人提到BlocksKit库,它具有类似的代码。请查看我的答案获取链接。 - Nate Cook
显示剩余3条评论

41

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

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

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

31
这个 "运作" 是巧合。它依赖于私有 API,Block 对象上的 invoke 方法并非公共的,也不是以这种方式使用的意图。 - bbum
1
Bbum:你说得对。我曾经认为-invoke是公共的,但我一直想更新我的答案并提交一个错误报告。 - lemnar
+1 很有趣,但内存管理(块的所有权)似乎是个问题。我认为这是一个只读块,这可能解释了为什么它不会崩溃。 - Steven Kramer
1
这似乎是一个很棒的解决方案,但我在想它是否符合苹果的要求,因为它使用了私有API。 - Brian
1
当传递nil而不是@selector(invoke)时起作用。 - k06a
显示剩余4条评论

18

不,选择器和代码块在Objective-C中不是兼容的类型(实际上,它们是非常不同的东西)。您需要编写自己的方法并传递其选择器。


11
特别地,一个选择器并不是你执行的内容;它是你发送给对象(或让另一个对象发送给第三个对象,就像这种情况:你告诉控件向目标发送一个[选择器放在这里]的消息)的名称。另一方面,一个块是你需要执行的内容:你直接调用块,而不依赖于一个对象。 - Peter Hosey

8
可以在UIButton的@selector参数中传递Objective-C块吗? 考虑到所有已经提供的答案,答案是肯定的,但需要进行一些设置类别的微小工作。
我建议使用NSInvocation,因为您可以做很多事情,例如计时器,存储为对象并调用等等...
以下是我所做的,但请注意我正在使用ARC。 首先是NSObject上的简单类别:
@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:repeats:)

重点是将您的块转换为NSInvocation更加灵活,可以这样使用:

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

再次声明,这只是一个建议。


还有一件事,这里调用的是一个公共方法。https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSInvocation_Class/Reference/Reference.html#//apple_ref/occ/instm/NSInvocation/invoke - Arvin

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”从未被释放。 - user102008

1

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

[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,并且是我正在测试的应用程序的简化版本(似乎可以工作,至少表面上如此,但不确定是否存在内存泄漏问题)。

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会将其销毁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接