Objective-C ile bir bloğu @selector olarak geçirebilir miyim?


90

A'daki @selectorargüman için bir Objective-C bloğunu geçmek mümkün müdür UIButton? yani, aşağıdakileri çalıştırmanın herhangi bir yolu var mı?

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

Teşekkürler

Yanıtlar:


69

Evet, ancak bir kategori kullanmanız gerekir.

Gibi bir şey:

@interface UIControl (DDBlockActions)

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

@end

Uygulama biraz daha yanıltıcı olabilir:

#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

Bazı açıklamalar:

  1. "Yalnızca dahili" adlı özel bir sınıf kullanıyoruz DDBlockActionWrapper. Bu, bir blok özelliğine (çağrılmasını istediğimiz blok) ve bu bloğu çağıran bir yönteme sahip basit bir sınıftır.
  2. UIControlKategori sadece bu sarma biri, o çağırılacak blok verir başlatır ve o zaman sargı ve kullanmak için kendisini gösterir invokeBlock:(normal) hedef ve eylem olarak yöntem.
  3. UIControlKategori bir dizi depolamak için bir ilgili nesneyi kullanan DDBlockActionWrappersiçin, UIControlhedeflerini korumaz. Bu dizi, blokların çağrılmaları gerektiğinde mevcut olmasını sağlamak içindir.
  4. DDBlockActionWrappersNesne yok edildiğinde temizlendiğinden emin olmalıyız, bu nedenle -[UIControl dealloc], ilişkili nesneyi kaldıran ve ardından orijinal deallockodu çağıran yeni bir şeyle kötü bir hile yapıyoruz . Zor, hileli. Aslında, ayırma işlemi sırasında ilişkili nesneler otomatik olarak temizlenir .

Son olarak, bu kod tarayıcıya yazılmıştır ve derlenmemiştir. Muhtemelen yanlış olan bazı şeyler vardır. Kilometreniz değişebilir.


4
Artık bu sorunu, ilişkili nesneleri kullanmaktan biraz daha verimli bir şekilde kullanabileceğinizi objc_implementationWithBlock()ve class_addMethod()çözebileceğinizi unutmayın (bu, yöntem araması kadar verimli olmayan bir karma arama anlamına gelir). Muhtemelen alakasız bir performans farkı, ancak bu bir alternatif.
bbum

@bbum demek istiyorsun imp_implementationWithBlock?
vikingosegundo

Evet - o. Bir zamanlar seçildi objc_implementationWithBlock(). :)
bbum

UITableViewCellHer yeni hedef yeni bir örnek olduğundan ve öncekiler aynı olaylar için temizlenmediğinden , özel 'lerde düğmeler için bunu kullanmak istenen hedef eylemlerinin tekrarlanmasına neden olacaktır. Önce hedefleri temizlemelisin for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene

Sanırım yukarıdaki kodu daha açık hale getiren bir şey, bir UIControl'ün birçok hedefi kabul edebileceğini bilmektir: eylem çiftleri .. bu nedenle, tüm bu çiftleri depolamak için değiştirilebilir bir dizi oluşturma ihtiyacı
abbood

41

Bloklar nesnelerdir. Gibi blok geçmek targetile, argüman @selector(invoke)olarak actionböyle argüman:

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

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

İlginç. Bu gece benzer bir şey yapıp yapamayacağıma bakacağım. Yeni bir soru başlatabilir.
Tad Donaghe

31
Bu tesadüfen "çalışır". Özel API'ye dayanır; invokeBlok nesneler üzerinde metot kamu ve bu şekilde kullanılmak üzere tasarlanmıştır değildir.
bbum

1
Bbum: Haklısın. -İnvoke'un halka açık olduğunu düşünmüştüm, ancak cevabımı güncellemek ve bir hata bildirmek istiyordum.
lemnar

1
harika bir çözüm gibi görünüyor, ancak özel bir API kullandığı için Apple tarafından kabul edilebilir olup olmadığını merak ediyorum.
Brian

1
Geçerken çalışır nilyerine @selector(invoke).
k06a

18

Hayır, Objective-C'de seçiciler ve bloklar uyumlu türler değildir (aslında bunlar çok farklı şeylerdir). Kendi yönteminizi yazmanız ve bunun yerine seçiciyi geçmeniz gerekecek.


11
Özellikle seçici, yürüttüğünüz bir şey değildir; bir nesneye gönderdiğiniz mesajın adıdır (veya başka bir nesnenin bu durumda olduğu gibi üçüncü bir nesneye göndermesini sağlayın: kontrole hedefe bir [seçici buraya gider] mesajı göndermesini söylüyorsunuz). Bir blok, diğer taraftan, bir Birbirinden bağımsız bir nesnenin doğrudan bloğunu çağırır: yürütmek şey.
Peter Hosey

8

Bir UIButton'da @selector argümanı için bir Objective-C bloğu geçirmek mümkün müdür?

Önceden verilen tüm cevapları dikkate aldığımızda, cevap Evet, ancak bazı kategorileri oluşturmak için küçük bir çalışma gerekiyor.

NSInvocation'ı kullanmanızı tavsiye ederim çünkü bununla zamanlayıcılarla, bir nesne olarak saklanan ve çağrılan ... vb.

İşte yaptığım şey, ancak ARC kullandığımı unutmayın.

Birincisi, NSObject'te basit bir kategoridir:

.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

Sırada NSInvocation'da bir blokta saklanacak bir kategori var:

.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

İşte nasıl kullanılacağı:

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

Çağrı ve standart Objective-C Yöntemleri ile çok şey yapabilirsiniz. Örneğin, NSInvocationOperation (initWithInvocation :), NSTimer (schedTimerWithTimeInterval: invocation: tekrarlar :)

Buradaki nokta, bloğunuzu bir NSInvocation'a dönüştürmek daha çok yönlüdür ve şu şekilde kullanılabilir:

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

Yine bu sadece bir öneridir.


Bir şey daha, burada çağırmak halka açık bir yöntemdir. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin

5

Maalesef o kadar basit değil.

Teoride, sınıfına dinamik olarak bir yöntem ekleyen target, bu yöntemin bir bloğun içeriğini yürütmesini sağlayan ve actionargümanın gerektirdiği şekilde bir seçici döndüren bir işlev tanımlamak mümkün olacaktır . Bu işlev , iOS durumunda, hala deneysel olan libffi'nin özel bir uygulamasına bağlı olan MABlockClosure tarafından kullanılan tekniği kullanabilir .

Eylemi bir yöntem olarak uygulamak daha iyi.


4

Kütüphane BlocksKit olarak da mevcuttur) bu özelliğe sahiptir.

UIControl + BlocksKit.h için başlık dosyasına bir göz atın. Dave DeLong'un fikrini uyguladılar, böylece sizin mecbur kalmazsınız. Bazı belgeler burada .


1

Birisi bana bunun neden yanlış olduğunu söyleyecek, belki ya da şans eseri, belki hayır, bu yüzden ya bir şeyler öğreneceğim ya da yardımcı olacağım.

Bunu birlikte attım. Gerçekten basit, sadece biraz dökümlü ince bir paketleyici. Bir uyarı kelimesi, çağırdığınız bloğun kullandığınız seçiciyle eşleşecek doğru imzaya sahip olduğunu varsayar (yani, argüman ve tür sayısı).

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

Ve

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

Gerçekten büyülü bir şey yok. void *Yöntemi çağırmadan önce, kullanılabilir bir blok imzasına çok sayıda aşağı tahmin ve tipleme. Açıkçası (aynen olduğu gibiperformSelector: ve ilişkili yöntem gibi, olası girdi kombinasyonları sonludur, ancak kodu değiştirirseniz genişletilebilir.

Bunun gibi kullanıldı:

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

Çıktıları:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Blok str = Test ile çağrıldı

Bir hedef eylem senaryosunda kullanıldığında, sadece şuna benzer bir şey yapmanız gerekir:

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

Hedef eylem sistemindeki hedef tutulmadığından, çağırma nesnesinin kontrolün kendisi olduğu sürece yaşadığından emin olmanız gerekir.

Benden daha uzman birinden bir şey duymakla ilgileniyorum.


bu hedef eylem senaryosunda bir bellek sızıntınız var çünkü invocationasla serbest bırakılmıyor
user102008

1

Bir UITableViewCell içindeki bir UIButton ile ilişkili bir eyleme ihtiyacım vardı. Her farklı hücredeki her düğmeyi izlemek için etiketleri kullanmaktan kaçınmak istedim. Bunu başarmanın en doğrudan yolunun, bir blok "eylemini" düğmeyle şu şekilde ilişkilendirmek olduğunu düşündüm:

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

Uygulamam biraz daha basitleştirildi, bahsettiğim için @bbum sayesinde imp_implementationWithBlockve class_addMethod(kapsamlı bir şekilde test edilmemiş olsa da):

#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'a (iOS SDK +5) sahip olmak işe yaramıyor mu? Bu kod ARC kullanıyor ve bunu test ettiğim bir Uygulamanın basitleştirilmesidir (en azından görünüşe göre, bellek sızdırıp sızdırmadığımdan emin değilim).

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

Tabii ki, bunun gerçek kullanım için ne kadar iyi olduğundan emin değilim. NSBlockOperation'a bir referansı canlı tutmanız gerekiyor, yoksa ARC'nin onu öldüreceğini düşünüyorum.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.