Objective-Cでブロックを@selectorとして渡すことはできますか?


90

@selector引数にObjective-Cブロックを渡すことは可能UIButtonですか?つまり、以下を機能させる方法はありますか?

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

ありがとう

回答:


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 '25年

@bbumどういう意味imp_implementationWithBlockですか?
vikingosegundo、2011年

うん、あれ。かつては命名されたobjc_implementationWithBlock()。:)
bbum

custom UITableViewCellのボタンにこれを使用すると、すべての新しいターゲットが新しいインスタンスであり、以前のターゲットが同じイベントに対してクリーンアップされないため、目的のターゲットアクションが重複します。最初にターゲットをクリーンアップする必要があります for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
ユージーン

アクションのペア...これらすべてのペアを格納するための可変配列を作成するので、必要性:私は上記のコードは、より明確に一つのことは、Uicontrolのは、多くのターゲットを受け入れることができることを知っていると思う
abbood

41

ブロックはオブジェクトです。あなたのブロックを渡すtargetと、引数@selector(invoke)としてactionこのような引数:

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

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

それは面白い。今夜も似たようなことができるか見てみます。新しい質問が始まるかもしれません。
Tad Donaghe、2011

31
これは偶然にも「うまくいく」。プライベートAPIに依存しています。invokeBlockオブジェクトのメソッドはパブリックではなく、この方法で使用するためのものではありません。
bbum '25年

1
Bbum:その通りです。-invokeは公開されていると思っていましたが、私の回答を更新してバグを報告するつもりでした。
lemnar 2011年

1
それは素晴らしいソリューションのようですが、プライベートAPIを使用しているため、Appleがそれを受け入れることができるかどうか疑問に思っています。
ブライアン、

1
nil代わりに渡されたときに動作します@selector(invoke)
k06a 2015

17

いいえ、セレクターとブロックはObjective-Cで互換性のある型ではありません(実際、それらはまったく異なります)。代わりに独自のメソッドを記述して、そのセレクターを渡す必要があります。


11
特に、セレクターはユーザーが実行するものではありません。これは、オブジェクトに送信するメッセージの名前です(または、この場合のように、別のオブジェクトに3番目のオブジェクトに送信させる:[selectors go here]メッセージをターゲットに送信するようにコントロールに指示しています)。一方、ブロック実行するものです。オブジェクトとは関係なく、ブロックを直接呼び出します。
Peter Hosey、2011年

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

繰り返しますが、これは1つの提案にすぎません。


もう1つ、ここで呼び出すのはパブリックメソッドです。developer.apple.com/library/mac/#documentation/Cocoa/Reference/...
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:)];

target-actionシステムのターゲットは保持されないため、コントロール自体が存続する限り、呼び出しオブジェクトが存続することを確認する必要があります。

私より専門家の意見を聞きたいです。


ので、あなたはそのターゲット・アクションのシナリオでのメモリリークを持ってinvocation解放されることはありません
user102008

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を使用しており、これは私がこれをテストしているアプリを簡略化したものです(少なくとも、どうやら動作しているようですが、メモリリークが発生しているかどうかはわかりません)。

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がそれを強制終了すると思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.