Bisakah saya melewatkan satu blok sebagai @selector dengan Objective-C?

90

Apakah mungkin untuk melewatkan blok Objective-C untuk @selectorargumen di a UIButton? yaitu, Apakah ada cara agar yang berikut ini berfungsi?

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

Terima kasih

Bill Shiff
sumber

Jawaban:

69

Ya, tetapi Anda harus menggunakan kategori.

Sesuatu seperti:

@interface UIControl (DDBlockActions)

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

@end

Penerapannya akan sedikit lebih rumit:

#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

Beberapa penjelasan:

  1. Kami menggunakan kelas "khusus internal" yang disebut DDBlockActionWrapper. Ini adalah kelas sederhana yang memiliki properti blok (blok yang ingin kita panggil), dan metode yang hanya memanggil blok itu.
  2. The UIControlkategori hanya instantiates salah satu pembungkus ini, memberikan blok yang akan dipanggil, dan kemudian memberitahu dirinya untuk menggunakan wrapper dan yang invokeBlock:metode sebagai target dan tindakan (seperti biasa).
  3. The UIControlkategori menggunakan obyek terkait untuk menyimpan array DDBlockActionWrappers, karena UIControltidak mempertahankan target. Larik ini untuk memastikan bahwa blok ada saat mereka seharusnya dipanggil.
  4. Kami harus memastikan bahwa DDBlockActionWrapperspembersihan ketika objek dihancurkan, jadi kami melakukan peretasan yang buruk dengan swizzling -[UIControl dealloc]yang baru yang menghapus objek terkait, dan kemudian memanggil deallockode asli . Rumit, rumit. Sebenarnya, objek terkait dibersihkan secara otomatis selama deallocation .

Terakhir, kode ini diketik di browser dan belum dikompilasi. Mungkin ada beberapa hal yang salah dengannya. Jarak tempuh Anda mungkin berbeda.

Dave DeLong
sumber
4
Perhatikan bahwa Anda sekarang dapat menggunakan objc_implementationWithBlock()dan class_addMethod()memecahkan masalah ini dengan cara yang sedikit lebih efisien daripada menggunakan objek terkait (yang menyiratkan pencarian hash yang tidak seefisien pencarian metode). Mungkin perbedaan kinerja yang tidak relevan, tetapi ini adalah alternatif.
bbum
@bbum maksudmu imp_implementationWithBlock?
vikingosegundo
Ya - yang itu. Itu pernah dinamai objc_implementationWithBlock(). :)
bbum
Menggunakan ini untuk tombol di kustom UITableViewCellakan menghasilkan duplikasi tindakan target yang diinginkan karena setiap target baru adalah instance baru dan target sebelumnya tidak dibersihkan untuk peristiwa yang sama. Anda harus membersihkan target terlebih dahulu for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene
Saya pikir satu hal yang membuat kode di atas lebih jelas adalah mengetahui bahwa UIControl dapat menerima banyak pasangan target: tindakan .. oleh karena itu perlu membuat larik yang bisa berubah untuk menyimpan semua pasangan itu
abbood
41

Balok adalah obyek. Berikan blok Anda sebagai targetargumen, dengan @selector(invoke)sebagai actionargumen, seperti ini:

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

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];
lemnar
sumber
Itu menarik. Saya akan melihat apakah saya dapat melakukan hal serupa malam ini. Mungkin memulai pertanyaan baru.
Tad Donaghe
31
Ini "bekerja" secara kebetulan. Ini bergantung pada API pribadi; yang invokemetode pada Blok benda tidak umum dan tidak dimaksudkan untuk digunakan dalam mode ini.
bbum
1
Bbum: Kamu benar. Saya pikir -invoke bersifat publik, tetapi saya bermaksud untuk memperbarui jawaban saya dan melaporkan bug.
lemnar
1
tampaknya solusi yang luar biasa, tetapi saya bertanya-tanya apakah itu dapat diterima oleh Apple karena menggunakan API pribadi.
Brian
1
Bekerja saat lulus, nilbukan @selector(invoke).
k06a
17

Tidak, penyeleksi dan blok bukanlah tipe yang kompatibel di Objective-C (sebenarnya, keduanya sangat berbeda). Anda harus menulis metode Anda sendiri dan meneruskan pemilihnya.

BoltClock
sumber
11
Secara khusus, pemilih bukanlah sesuatu yang Anda jalankan; ini adalah nama pesan yang Anda kirim ke suatu objek (atau objek lain dikirim ke objek ketiga, seperti dalam kasus ini: Anda memberi tahu kontrol untuk mengirim pesan [selector go here] ke target). Sebaliknya, blok adalah sesuatu yang Anda jalankan: Anda memanggil blok secara langsung, terlepas dari suatu objek.
Peter Hosey
7

Apakah mungkin untuk melewatkan blok Objective-C untuk argumen @selector di UIButton?

Mengambil semua jawaban yang sudah disediakan, jawabannya adalah Ya tetapi sedikit pekerjaan diperlukan untuk menyiapkan beberapa kategori.

Saya merekomendasikan menggunakan NSInvocation karena Anda dapat melakukan banyak hal dengan ini seperti dengan pengatur waktu, disimpan sebagai objek dan dipanggil ... dll ...

Inilah yang saya lakukan, tetapi perhatikan bahwa saya menggunakan ARC.

Pertama adalah kategori sederhana di 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

Berikutnya adalah kategori di NSInvocation untuk disimpan dalam satu blok:

.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

Berikut cara menggunakannya:

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

Anda dapat melakukan banyak hal dengan pemanggilan dan Metode Objective-C standar. Misalnya, Anda dapat menggunakan NSInvocationOperation (initWithInvocation :), NSTimer (scheduleTimerWithTimeInterval: invocation: repeates :)

Intinya adalah mengubah blok Anda menjadi NSInvocation lebih fleksibel dan dapat digunakan sebagai berikut:

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

Sekali lagi ini hanyalah satu saran.

Arvin
sumber
Satu hal lagi, panggil di sini adalah metode publik. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin
5

Sayangnya tidak sesederhana itu.

Secara teori, dimungkinkan untuk mendefinisikan fungsi yang secara dinamis menambahkan metode ke kelas target, meminta metode tersebut mengeksekusi isi blok, dan mengembalikan selektor sesuai kebutuhan oleh actionargumen. Fungsi ini dapat menggunakan teknik yang digunakan oleh MABlockClosure , yang, dalam kasus iOS, bergantung pada implementasi kustom libffi, yang masih eksperimental.

Anda lebih baik menerapkan tindakan sebagai metode.

Quinn Taylor
sumber
4

Pustaka BlockKit di Github (juga tersedia sebagai CocoaPod) memiliki fitur bawaan ini.

Lihatlah file header untuk UIControl + BlocksKit.h. Mereka telah menerapkan ide Dave DeLong jadi Anda tidak perlu melakukannya. Beberapa dokumentasi ada di sini .

Nate Cook
sumber
1

Seseorang akan memberi tahu saya mengapa ini salah, mungkin, atau dengan sedikit keberuntungan, mungkin tidak, jadi saya akan belajar sesuatu, atau saya akan membantu.

Saya baru saja melempar ini bersama-sama. Ini sangat mendasar, hanya pembungkus tipis dengan sedikit pengecoran. Sebuah kata peringatan, mengasumsikan blok yang Anda panggil memiliki tanda tangan yang benar untuk mencocokkan pemilih yang Anda gunakan (yaitu jumlah argumen dan jenis).

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

Dan

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

Benar-benar tidak ada hal ajaib yang terjadi. Hanya banyak downcasting void *dan typecasting ke tanda tangan blok yang dapat digunakan sebelum menjalankan metode ini. Jelas (seperti dengan performSelector:dan metode terkait, kemungkinan kombinasi input terbatas, tetapi dapat diperpanjang jika Anda memodifikasi kode.

Digunakan seperti ini:

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

Ini menghasilkan:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Block telah dipanggil dengan str = Test

Digunakan dalam skenario tindakan target, Anda hanya perlu melakukan sesuatu seperti ini:

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

Karena target dalam sistem target-action tidak dipertahankan, Anda perlu memastikan objek pemanggilan hidup selama kontrol itu sendiri berjalan.

Saya tertarik untuk mendengar apa pun dari seseorang yang lebih ahli daripada saya.

d11wtq.dll
sumber
Anda mengalami kebocoran memori dalam skenario tindakan-target itu karena invocationtidak pernah dirilis
user102008
1

Saya perlu memiliki tindakan yang terkait dengan UIButton dalam UITableViewCell. Saya ingin menghindari penggunaan tag untuk melacak setiap tombol di setiap sel yang berbeda. Saya pikir cara paling langsung untuk mencapai ini adalah dengan mengaitkan "aksi" blok ke tombol seperti ini:

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

Implementasi saya sedikit lebih sederhana, berkat @bbum untuk menyebutkan imp_implementationWithBlockdan class_addMethod, (meskipun tidak diuji secara ekstensif):

#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
Don Miguel
sumber
0

Tidakkah bekerja memiliki NSBlockOperation (iOS SDK +5). Kode ini menggunakan ARC dan ini adalah penyederhanaan dari Aplikasi yang saya gunakan untuk menguji ini (tampaknya berfungsi, setidaknya tampaknya, tidak yakin apakah saya membocorkan memori).

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

Tentu saja, saya tidak yakin seberapa bagus ini untuk penggunaan nyata. Anda perlu menjaga referensi ke NSBlockOperation tetap hidup atau menurut saya ARC akan membunuhnya.

rufo
sumber