¿Puedo pasar un bloque como @selector con Objective-C?

90

¿Es posible pasar un bloque Objective-C para el @selectorargumento en a UIButton? es decir, ¿hay alguna forma de hacer que funcione lo siguiente?

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

Gracias

Bill Shiff
fuente

Respuestas:

69

Sí, pero tendrías que usar una categoría.

Algo como:

@interface UIControl (DDBlockActions)

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

@end

La implementación sería un poco más 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

Alguna explicación:

  1. Estamos usando una clase personalizada "solo interna" llamada DDBlockActionWrapper. Esta es una clase simple que tiene una propiedad de bloque (el bloque que queremos que se invoque) y un método que simplemente invoca ese bloque.
  2. La UIControlcategoría simplemente crea una instancia de uno de estos envoltorios, le da el bloque que se invocará y luego se dice a sí misma que use ese envoltorio y su invokeBlock:método como destino y acción (como es normal).
  3. La UIControlcategoría usa un objeto asociado para almacenar una matriz DDBlockActionWrappers, porque UIControlno retiene sus objetivos. Esta matriz es para garantizar que los bloques existan cuando se supone que deben invocarse.
  4. Tenemos que asegurarnos de que DDBlockActionWrappersse limpien cuando se destruye el objeto, por lo que estamos haciendo un truco desagradable de swizzling -[UIControl dealloc]con uno nuevo que elimina el objeto asociado y luego invoca el dealloccódigo original . Difícil, complicado. En realidad, los objetos asociados se limpian automáticamente durante la desasignación .

Finalmente, este código se escribió en el navegador y no se ha compilado. Probablemente haya algunas cosas mal en él. Su experiencia puede ser diferente.

Dave DeLong
fuente
4
Tenga en cuenta que ahora puede usar objc_implementationWithBlock()y class_addMethod()para resolver este problema de una manera un poco más eficiente que usando objetos asociados (lo que implica una búsqueda de hash que no es tan eficiente como la búsqueda de métodos). Probablemente una diferencia de rendimiento irrelevante, pero es una alternativa.
bbum
@bbum ¿quieres decir imp_implementationWithBlock?
vikingosegundo
Sí, ese. Una vez fue nombrado objc_implementationWithBlock(). :)
bbum
El uso de esto para los botones en los personalizados UITableViewCelldará como resultado la duplicación de objetivos-acciones deseados, ya que cada nuevo objetivo es una nueva instancia y los anteriores no se limpian para los mismos eventos. Primero tienes que limpiar los objetivos for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene
Creo que una cosa que hace que el código anterior sea más claro es saber que un UIControl puede aceptar muchos pares objetivo: acción ... de ahí la necesidad de crear una matriz mutable para almacenar todos esos pares
abbood
41

Los bloques son objetos. Pase su bloque como targetargumento, con @selector(invoke)como actionargumento, así:

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

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];
lemnar
fuente
Eso es interesante. Veré si puedo hacer algo similar esta noche. Puede comenzar una nueva pregunta.
Tad Donaghe
31
Esto "funciona" por casualidad. Se basa en una API privada; el invokemétodo de los objetos Block no es público y no está destinado a utilizarse de esta manera.
bbum
1
Bbum: Tienes razón. Pensé que -invoke era público, pero tenía la intención de actualizar mi respuesta y presentar un error.
lemnar
1
parece una solución increíble, pero me pregunto si es aceptable por Apple ya que utiliza una API privada.
Brian
1
Funciona cuando se pasa en nillugar de @selector(invoke).
k06a
17

No, los selectores y bloques no son tipos compatibles en Objective-C (de hecho, son cosas muy diferentes). Tendrá que escribir su propio método y pasar su selector en su lugar.

BoltClock
fuente
11
En particular, un selector no es algo que se ejecuta; es el nombre del mensaje que envía a un objeto (o hacer que otro objeto se envíe a un tercer objeto, como en este caso: le está diciendo al control que envíe un mensaje [el selector va aquí] al objetivo). Un bloque, por otro lado, es algo que ejecutas: llamas al bloque directamente, independientemente de un objeto.
Peter Hosey
7

¿Es posible pasar un bloque Objective-C para el argumento @selector en un UIButton?

Tomando todas las respuestas ya proporcionadas, la respuesta es Sí, pero es necesario un poco de trabajo para configurar algunas categorías.

Recomiendo usar NSInvocation porque puede hacer mucho con esto, como con temporizadores, almacenados como un objeto e invocados ... etc ...

Esto es lo que hice, pero tenga en cuenta que estoy usando ARC.

Primero hay una categoría simple en NSObject:

.h

@interface NSObject (CategoryNSObject)

- (void) associateValue:(id)value withKey:(NSString *)aKey;
- (id) associatedValueForKey:(NSString *)aKey;

@end

.metro

#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

La siguiente es una categoría en NSInvocation para almacenar en un bloque:

.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

.metro

#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

Así es como se usa:

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

Puede hacer mucho con la invocación y los métodos estándar de Objective-C. Por ejemplo, puede utilizar NSInvocationOperation (initWithInvocation :), NSTimer (scheduleTimerWithTimeInterval: invocation: repeates :)

El punto es convertir su bloque en una NSInvocation es más versátil y se puede usar como tal:

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

Nuevamente, esta es solo una sugerencia.

Arvin
fuente
Una cosa más, invocar aquí es un método público. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin
5

No tan simple como eso, desafortunadamente.

En teoría, sería posible definir una función que agregue dinámicamente un método a la clase de target, hacer que ese método ejecute el contenido de un bloque y devolver un selector según lo necesite el actionargumento. Esta función podría utilizar la técnica utilizada por MABlockClosure , que, en el caso de iOS, depende de una implementación personalizada de libffi, que aún es experimental.

Es mejor implementar la acción como método.

Quinn Taylor
fuente
4

La biblioteca BlocksKit en Github (también disponible como CocoaPod) tiene esta función incorporada.

Eche un vistazo al archivo de encabezado de UIControl + BlocksKit.h. Han implementado la idea de Dave DeLong para que usted no tenga que hacerlo. Alguna documentación está aquí .

Nate Cook
fuente
1

Alguien me va a decir por qué esto está mal, tal vez, o con suerte, tal vez no, así que aprenderé algo o seré útil.

Acabo de lanzar esto junto. Es realmente básico, solo una envoltura delgada con un poco de yeso. Una advertencia, asume que el bloque que estás invocando tiene la firma correcta para coincidir con el selector que usas (es decir, número de argumentos y 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

Y

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

Realmente no está sucediendo nada mágico. Solo un montón de downcasting void *y encasillado a una firma de bloque utilizable antes de invocar el método. Obviamente (al igual que conperformSelector: método asociado, las posibles combinaciones de entradas son finitas, pero ampliables si modificas el código.

Usado así:

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

Produce:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Se invocó el bloque con str = Test

Usado en un escenario de acción objetivo, solo necesita hacer algo como esto:

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

Dado que el destino en un sistema de acción de destino no se retiene, deberá asegurarse de que el objeto de invocación dure tanto tiempo como el control.

Me interesa saber algo de alguien más experto que yo.

d11wtq
fuente
tiene una pérdida de memoria en ese escenario de acción de destino porque invocationnunca se publica
user102008
1

Necesitaba tener una acción asociada a un UIButton dentro de un UITableViewCell. Quería evitar el uso de etiquetas para rastrear cada botón en cada celda diferente. Pensé que la forma más directa de lograr esto era asociar una "acción" de bloque al botón así:

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

Mi implementación es un poco más simplificada, gracias a @bbum por mencionar imp_implementationWithBlocky class_addMethod, (aunque no se ha probado exhaustivamente):

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

¿No funciona tener una NSBlockOperation (iOS SDK +5)? Este código usa ARC y es una simplificación de una aplicación con la que estoy probando esto (parece funcionar, al menos aparentemente, no estoy seguro de si estoy perdiendo memoria).

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

Por supuesto, no estoy seguro de qué tan bueno es esto para un uso real. Necesitas mantener viva una referencia a NSBlockOperation o creo que ARC lo matará.

rufo
fuente