Czy mogę przekazać blok jako @selector z Objective-C?

90

Czy jest możliwe przekazanie bloku Objective-C dla @selectorargumentu w UIButton? tj. czy istnieje sposób, aby następujące elementy działały?

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

Dzięki

Bill Shiff
źródło

Odpowiedzi:

69

Tak, ale musiałbyś użyć kategorii.

Coś jak:

@interface UIControl (DDBlockActions)

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

@end

Wdrożenie byłoby nieco trudniejsze:

#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

Kilka wyjaśnień:

  1. Używamy niestandardowej klasy „tylko do użytku wewnętrznego” o nazwie DDBlockActionWrapper. Jest to prosta klasa, która ma właściwość block (blok, który chcemy wywołać) oraz metodę, która po prostu wywołuje ten blok.
  2. UIControlKategoria prostu instancję jednego z tych opakowań, daje to blok się powoływać, a następnie informuje się do korzystania z tego opakowania i jego invokeBlock:metodę jako cel i działania (jak zwykle).
  3. UIControlKategoria wykorzystuje wiązany obiekt do przechowywania tablicę DDBlockActionWrappers, ponieważ UIControlnie zachowuje swoje cele. Ta tablica ma zapewnić, że bloki istnieją w momencie, gdy mają zostać wywołane.
  4. Musimy upewnić się, że DDBlockActionWrapperszostanie wyczyszczony, gdy obiekt zostanie zniszczony, więc robimy nieprzyjemny hack polegający na wymieszaniu -[UIControl dealloc]z nowym, który usuwa powiązany obiekt, a następnie wywołuje oryginalny deallockod. Podstępne, podstępne. W rzeczywistości powiązane obiekty są czyszczone automatycznie podczas cofania przydziału .

Ostatecznie ten kod został wpisany w przeglądarce i nie został skompilowany. Prawdopodobnie jest z nim nie tak. Twój przebieg może się różnić.

Dave DeLong
źródło
4
Zauważ, że możesz teraz użyć objc_implementationWithBlock()i class_addMethod()rozwiązać ten problem w nieco bardziej efektywny sposób niż używanie skojarzonych obiektów (co implikuje wyszukiwanie skrótu, które nie jest tak wydajne jak wyszukiwanie metod). Prawdopodobnie nieistotna różnica w wydajności, ale jest to alternatywa.
bbum
@bbum masz na myśli imp_implementationWithBlock?
vikingosegundo
Tak - ten. Kiedyś został nazwany objc_implementationWithBlock(). :)
bbum
Użycie tego dla przycisków w niestandardowych UITableViewCellspowoduje powielenie żądanych działań-celów, ponieważ każdy nowy cel jest nową instancją, a poprzednie nie są czyszczone dla tych samych zdarzeń. Najpierw musisz wyczyścić cele for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene
Myślę, że jedną rzeczą, która sprawia, że ​​powyższy kod jest bardziej przejrzysty, jest świadomość, że UIControl może zaakceptować wiele par cel: akcja ... stąd potrzeba stworzenia zmiennej tablicy do przechowywania wszystkich tych par
abbood
41

Bloki to obiekty. Przekaż swój blok jako targetargument, @selector(invoke)jako actionargument, na przykład:

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

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];
lemnar
źródło
To interesujące. Sprawdzę, czy uda mi się dziś wieczorem zrobić coś podobnego. Może rozpocząć nowe pytanie.
Tad Donaghe
31
To „działa” przez przypadek. Opiera się na prywatnym API; invokemetoda na obiektach blok nie jest publiczny i nie są przeznaczone do wykorzystania w ten sposób.
bbum
1
Bbum: Masz rację. Myślałem, że -invoke jest publiczne, ale chciałem zaktualizować swoją odpowiedź i zgłosić błąd.
lemnar
1
wydaje się to niesamowite rozwiązanie, ale zastanawiam się, czy jest to akceptowalne przez Apple, ponieważ korzysta z prywatnego interfejsu API.
Brian
1
Działa po przekazaniu nilzamiast @selector(invoke).
k06a
17

Nie, selektory i bloki nie są kompatybilnymi typami w Objective-C (w rzeczywistości są to bardzo różne rzeczy). Będziesz musiał napisać własną metodę i zamiast tego przekazać jej selektor.

BoltClock
źródło
11
W szczególności selektor nie jest czymś, co wykonujesz; jest to nazwa wiadomości, którą wysyłasz do obiektu (lub wysyłasz inny obiekt do trzeciego obiektu, jak w tym przypadku: mówisz kontrolce, aby wysłała wiadomość [selector idzie tutaj] do celu). Z drugiej strony blok to coś, co wykonujesz: wywołujesz blok bezpośrednio, niezależnie od obiektu.
Peter Hosey,
7

Czy można przekazać blok Objective-C dla argumentu @selector w UIButton?

Biorąc pod uwagę wszystkie udzielone już odpowiedzi, odpowiedź brzmi: tak, ale konfiguracja niektórych kategorii wymaga odrobiny pracy.

Polecam używanie NSInvocation, ponieważ możesz wiele z tym zrobić, na przykład z licznikami czasu, przechowywanymi jako obiekt i wywoływanymi ... itd.

Oto co zrobiłem, ale zauważ, że używam ARC.

Pierwsza to prosta kategoria na 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

Dalej jest kategoria w NSInvocation do przechowywania w bloku:

.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

Oto jak z niego korzystać:

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

Możesz wiele zrobić za pomocą wywołania i standardowych metod Objective-C. Na przykład możesz użyć NSInvocationOperation (initWithInvocation :), NSTimer (scheduleTimerWithTimeInterval: invocation: repeates :)

Chodzi o to, że przekształcenie twojego bloku w NSInvocation jest bardziej wszechstronne i może być używane jako takie:

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

To tylko jedna sugestia.

Arvin
źródło
Jeszcze jedno, invoke to metoda publiczna. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin
5

Niestety nie jest to takie proste.

W teorii byłoby możliwe zdefiniowanie funkcji, która dynamicznie dodaje metodę do klasy of target, powoduje wykonanie przez tę metodę zawartości bloku i zwrócenie selektora zgodnie z wymaganiami actionargumentu. Ta funkcja może wykorzystywać technikę używaną przez MABlockClosure , która w przypadku iOS zależy od niestandardowej implementacji libffi, która wciąż jest eksperymentalna.

Lepiej zaimplementuj akcję jako metodę.

Quinn Taylor
źródło
4

Biblioteka BlocksKit na Github (dostępna również jako CocoaPod) ma wbudowaną tę funkcję.

Spójrz na plik nagłówkowy UIControl + BlocksKit.h. Wdrożyli pomysł Dave'a DeLonga, więc nie musisz. Część dokumentacji jest tutaj .

Nate Cook
źródło
1

Ktoś mi powie, dlaczego to jest złe, może lub przy odrobinie szczęścia, może nie, więc albo się czegoś nauczę, albo będę pomocny.

Po prostu rzuciłem to razem. To naprawdę proste, po prostu cienkie opakowanie z odrobiną odlewu. Słowo ostrzeżenia, zakłada, że ​​wywoływany blok ma poprawną sygnaturę pasującą do selektora, którego używasz (tj. Liczbę argumentów i typów).

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

I

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

Naprawdę nie dzieje się nic magicznego. Tylko dużo downcastingu void *i typecastingu do użytecznego podpisu blokowego przed wywołaniem metody. Oczywiście (podobnie jak w przypadku performSelector:metody i powiązanej metody, możliwe kombinacje danych wejściowych są ograniczone, ale można je rozszerzyć, jeśli zmodyfikujesz kod.

Używane w ten sposób:

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

Wyprowadza:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Blok został wywołany z str = Test

Używany w scenariuszu typu cel-akcja, wystarczy zrobić coś takiego:

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

Ponieważ cel w systemie cel-akcja nie jest zachowany, będziesz musiał upewnić się, że obiekt wywołania będzie żył tak długo, jak długo to robi.

Chciałbym usłyszeć coś od kogoś bardziej eksperta niż ja.

d11wtq
źródło
masz wyciek pamięci w tym scenariuszu działania celu, ponieważ invocationnigdy nie został wydany
user102008
1

Musiałem mieć akcję skojarzoną z UIButton w ramach UITableViewCell. Chciałem uniknąć używania tagów do śledzenia każdego przycisku w każdej innej komórce. Pomyślałem, że najbardziej bezpośrednim sposobem osiągnięcia tego jest skojarzenie bloku „akcja” z przyciskiem w następujący sposób:

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

Moja implementacja jest nieco bardziej uproszczona, dzięki @bbum za wzmiankę imp_implementationWithBlocki class_addMethod(choć nie dogłębnie przetestowane):

#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
źródło
0

Nie działa, aby mieć NSBlockOperation (iOS SDK +5). Ten kod używa ARC i jest uproszczeniem aplikacji, w której to testuję (wydaje się działać, przynajmniej pozornie, nie jestem pewien, czy wyciekam pamięć).

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

Oczywiście nie jestem pewien, jak dobre jest to w prawdziwym użyciu. Musisz zachować przy życiu odniesienie do NSBlockOperation albo myślę, że ARC go zabije.

rufo
źródło