Posso passar um bloco como @selector com Objective-C?

90

É possível passar um bloco Objective-C para o @selectorargumento em a UIButton? ou seja, existe alguma maneira de fazer o seguinte funcionar?

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

obrigado

Bill Shiff
fonte

Respostas:

69

Sim, mas você teria que usar uma categoria.

Algo como:

@interface UIControl (DDBlockActions)

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

@end

A implementação seria um pouco mais complicada:

#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

Alguma explicação:

  1. Estamos usando uma classe personalizada "apenas interna" chamada DDBlockActionWrapper. Esta é uma classe simples que possui uma propriedade de bloco (o bloco que queremos invocar) e um método que simplesmente invoca esse bloco.
  2. A UIControlcategoria simplesmente instancia um desses invólucros, fornece a ele o bloco a ser invocado e, a seguir, diz a si mesma para usar esse invólucro e seu invokeBlock:método como destino e ação (como normal).
  3. A UIControlcategoria usa um objeto associado para armazenar um array de DDBlockActionWrappers, porque UIControlnão retém seus alvos. Essa matriz é para garantir que os blocos existam quando deveriam ser invocados.
  4. Temos que garantir que DDBlockActionWrapperssejam limpos quando o objeto for destruído, então estamos fazendo um hack desagradável de swizzling -[UIControl dealloc]com um novo que remove o objeto associado e, em seguida, invoca o dealloccódigo original . Complicado, complicado. Na verdade, os objetos associados são limpos automaticamente durante a desalocação .

Finalmente, este código foi digitado no navegador e não foi compilado. Provavelmente há algumas coisas erradas com ele. Sua milhagem pode variar.

Dave DeLong
fonte
4
Observe que agora você pode usar objc_implementationWithBlock()e class_addMethod()para resolver esse problema de uma maneira um pouco mais eficiente do que usar objetos associados (o que implica em uma pesquisa de hash que não é tão eficiente quanto a pesquisa de método). Provavelmente uma diferença de desempenho irrelevante, mas é uma alternativa.
bbum
@bbum você quer dizer imp_implementationWithBlock?
vikingosegundo
Sim - aquele. Já foi nomeado objc_implementationWithBlock(). :)
bbum
Usar isso para botões em custom UITableViewCell's resultará na duplicação de alvos-ações desejados, já que cada novo alvo é uma nova instância e os anteriores não são limpos para os mesmos eventos. Você tem que limpar os alvos primeiro for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene
Acho que uma coisa que torna o código acima mais claro é saber que um UIControl pode aceitar muitos pares alvo: ação .. daí a necessidade de criar uma matriz mutável para armazenar todos esses pares
abbood
41

Os blocos são objetos. Passe seu bloco como o targetargumento, com @selector(invoke)como o actionargumento, assim:

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

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];
Lemnar
fonte
Isso é interessante. Vou ver se consigo fazer algo semelhante esta noite. Pode iniciar uma nova pergunta.
Tad Donaghe
31
Isso "funciona" por coincidência. Ele se baseia em API privada; o invokemétodo em objetos Block não é público e não se destina a ser usado dessa maneira.
bbum
1
Bbum: Você está certo. Achei que -invoke fosse público, mas queria atualizar minha resposta e registrar um bug.
Lemnar,
1
parece uma solução incrível, mas estou me perguntando se é aceitável pela Apple, pois usa uma API privada.
Brian de
1
Funciona quando aprovado em nilvez de @selector(invoke).
k06a
17

Não, seletores e blocos não são tipos compatíveis em Objective-C (na verdade, eles são coisas muito diferentes). Você terá que escrever seu próprio método e passar seu seletor.

BoltClock
fonte
11
Em particular, um seletor não é algo que você executa; é o nome da mensagem que você envia para um objeto (ou faz com que outro objeto seja enviado para um terceiro objeto, como neste caso: você está dizendo ao controle para enviar uma mensagem [o seletor entra aqui] para o destino). Um bloco, por outro lado, é algo que você executa: você chama o bloco diretamente, independentemente de um objeto.
Peter Hosey
7

É possível passar um bloco Objective-C para o argumento @selector em um UIButton?

Considerando todas as respostas já fornecidas, a resposta é Sim, mas um pouco de trabalho é necessário para configurar algumas categorias.

Eu recomendo usar NSInvocation porque você pode fazer muito com isso, como temporizadores, armazenados como um objeto e invocados ... etc ...

Aqui está o que eu fiz, mas observe que estou usando o ARC.

A primeira é uma categoria simples no 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

A seguir está uma categoria em NSInvocation para armazenar em um bloco:

.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

Aqui está como usá-lo:

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

Você pode fazer muito com a invocação e os métodos Objective-C padrão. Por exemplo, você pode usar NSInvocationOperation (initWithInvocation :), NSTimer (storedTimerWithTimeInterval: invocation: repeates :)

O ponto é transformar seu bloco em um NS. O Invocation é mais versátil e pode ser usado como tal:

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

Novamente, esta é apenas uma sugestão.

Arvin
fonte
Mais uma coisa, invocar aqui é um método público. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin
5

Não é tão simples assim, infelizmente.

Em teoria, seria possível definir uma função que adiciona dinamicamente um método à classe de target, fazer com que esse método execute o conteúdo de um bloco e retornar um seletor conforme necessário para o actionargumento. Esta função poderia utilizar a técnica utilizada pelo MABlockClosure , que, no caso do iOS, depende de uma implementação customizada de libffi, que ainda é experimental.

É melhor você implementar a ação como um método.

Quinn Taylor
fonte
4

A biblioteca BlocksKit no Github (também disponível como CocoaPod) possui esse recurso integrado.

Dê uma olhada no arquivo de cabeçalho de UIControl + BlocksKit.h. Eles implementaram a ideia de Dave DeLong para que você não precise fazer isso. Alguma documentação está aqui .

Nate Cook
fonte
1

Alguém vai me dizer por que isso está errado, talvez, ou com alguma sorte, talvez não, então ou aprenderei algo ou serei útil.

Eu apenas juntei isso. É realmente básico, apenas uma embalagem fina com um pouco de fundição. Uma palavra de advertência, ele assume que o bloco que você está invocando tem a assinatura correta para corresponder ao seletor que você usa (isto é, número de argumentos e tipos).

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

E

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

Não há realmente nada mágico acontecendo. Apenas muito downcasting void *e typecasting para uma assinatura de bloco utilizável antes de invocar o método. Obviamente (assim como com performSelector:e método associado, as combinações possíveis de entradas são finitas, mas extensíveis se você modificar o código.

Usado assim:

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

Ele produz:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] O bloco foi invocado com str = Test

Usado em um cenário de ação-alvo, você só precisa fazer algo assim:

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

Como o alvo em um sistema de ação de destino não é retido, você precisará garantir que o objeto de invocação viva enquanto o próprio controle durar.

Estou interessado em ouvir qualquer coisa de alguém mais especialista do que eu.

d11wtq
fonte
você tem um vazamento de memória nesse cenário de ação-alvo porque invocationnunca foi lançado
user102008
1

Eu precisava ter uma ação associada a um UIButton dentro de um UITableViewCell. Eu queria evitar o uso de tags para rastrear cada botão em cada célula diferente. Achei que a maneira mais direta de conseguir isso era associar uma "ação" de bloqueio ao botão, assim:

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

Minha implementação é um pouco mais simplificada, graças a @bbum por mencionar imp_implementationWithBlocke class_addMethod, (embora não tenha sido exaustivamente testado):

#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
fonte
0

Não funciona ter um NSBlockOperation (iOS SDK +5). Este código usa ARC e é uma simplificação de um aplicativo com o qual estou testando (parece funcionar, pelo menos aparentemente, não tenho certeza se estou perdendo memória).

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

Claro, não tenho certeza de como isso é bom para uso real. Você precisa manter uma referência ao NSBlockOperation viva ou acho que o ARC irá eliminá-lo.

Rufo
fonte