É possível passar um bloco Objective-C para o argumento @selector
em um UIButton
? ou seja, existe alguma maneira de fazer o seguinte funcionar?
[closeOverlayButton addTarget:self action:^ {[anotherIvarLocalToThisMethod removeFromSuperview];} forControlEvents:UIControlEventTouchUpInside];
obrigado
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 @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:
DDBlockActionWrapper
. Esta é uma class simples que possui uma propriedade de bloco (o bloco que queremos chamar), e um método que simplesmente invoca esse bloco. UIControl
simplesmente instancia um desses wrappers, fornece o bloco a ser invocado e diz a si mesmo para usar esse wrapper e seu método invokeBlock:
como o destino e a ação (como normalmente). UIControl
usa um object associado para armazenar uma matriz de DDBlockActionWrappers
, porque o DDBlockActionWrappers
não mantém seus destinos. Essa matriz é para garantir que os blocos existam quando devem ser invocados. DDBlockActionWrappers
sejam limpos quando o object é destruído, então estamos fazendo um ataque desagradável de “swizzling out” -[UIControl dealloc]
dealoc de UIControl -[UIControl dealloc]
com um novo que remove o object associado, e então invoca o código dealloc
original.Finalmente, esse código foi typescript no navegador e não foi compilado. Há provavelmente algumas coisas erradas com isso. Sua milhagem pode variar.
Blocos são objects. Passe seu bloco como o argumento de target
, com @selector(invoke)
como o argumento de action
, assim:
id block = [^{NSLog(@"Hello, world");} copy];// Don't forget to -release. [button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside];
Não, seletores e blocos não são compatíveis no Objective-C (na verdade, são coisas muito diferentes). Você terá que escrever seu próprio método e passar seu seletor.
É possível passar um bloco Objective-C para o argumento @selector em um UIButton?
Tomando em 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 com timeres, armazenados como um object e invocado … 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 @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
Em seguida é 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 methods padrão do Objective-C. Por exemplo, você pode usar NSInvocationOperation (initWithInvocation :), NSTimer (scheduledTimerWithTimeInterval: invocation: repeates 🙂
O ponto é transformar o seu bloco em um NSInvocation é 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];
Mais uma vez, esta é apenas uma sugestão.
Não é tão simples assim, infelizmente.
Em teoria, seria possível definir uma function que adiciona dinamicamente um método à class de target
, fazer com que esse método execute o conteúdo de um bloco e retornar um seletor conforme necessário pelo argumento de action
. Esta function poderia usar a técnica usada pelo MABlockClosure , que, no caso do iOS, depende de uma implementação customizada do libffi, que ainda é experimental.
É melhor implementar a ação como um método.
A biblioteca BlocksKit no Github (também disponível como CocoaPod) tem esse recurso embutido.
Dê uma olhada no arquivo de header do UIControl + BlocksKit.h. Eles implementaram a ideia de Dave DeLong para que você não precise. Alguma documentação está aqui .
Alguém vai me dizer por que isso está errado, talvez, ou com alguma sorte, talvez não, então vou aprender alguma coisa, ou serei útil.
Eu só joguei isso juntos. É realmente básico, apenas um invólucro fino com um pouco de fundição. Uma palavra de advertência, 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 @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 de mágico acontecendo. Apenas muitos downcasting para void *
e typecasting para uma assinatura de bloco usável antes de invocar o método. Obviamente (assim como com performSelector:
e método associado, as combinações possíveis de inputs 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 = teste
Usado em um cenário de ação de destino, 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 destino em um sistema de ação de destino não é retido, você precisará assegurar que o object de chamada viva pelo tempo que o próprio controle fizer.
Estou interessado em ouvir qualquer coisa de alguém mais experiente que eu.
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. Eu pensei que a maneira mais direta de conseguir isso era associar um bloco “ação” ao botão da seguinte forma:
[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 ao @bbum por mencionar imp_implementationWithBlock
e class_addMethod
, (embora não tenha sido extensivamente testado):
#import @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
Não funciona para ter um NSBlockOperation (iOS SDK +5). Este código usa o ARC e é uma simplificação de um aplicativo que estou testando com isso (parece funcionar, pelo menos aparentemente, não tenho certeza se estou vazando memory).
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 que não tenho certeza de como isso é bom para uso real. Você precisa manter uma referência ao NSBlockOperation vivo ou acho que o ARC irá eliminá-lo.