Posso passare un blocco come @selector con Objective-C?


90

È possibile passare un blocco Objective-C per l' @selectorargomento in a UIButton? cioè, c'è un modo per far funzionare quanto segue?

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

Grazie

Risposte:


69

Sì, ma dovresti usare una categoria.

Qualcosa di simile a:

@interface UIControl (DDBlockActions)

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

@end

L'implementazione sarebbe un po 'più complicata:

#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

Qualche spiegazione:

  1. Stiamo usando una classe personalizzata "solo interna" chiamata DDBlockActionWrapper. Questa è una classe semplice che ha una proprietà block (il blocco che vogliamo venga richiamato) e un metodo che richiama semplicemente quel blocco.
  2. La UIControlcategoria crea semplicemente un'istanza di uno di questi wrapper, gli fornisce il blocco da invocare e poi dice a se stessa di usare quel wrapper e il suo invokeBlock:metodo come obiettivo e azione (normalmente).
  3. La UIControlcategoria utilizza un oggetto associato per memorizzare un array di DDBlockActionWrappers, perché UIControlnon mantiene i suoi obiettivi. Questo array serve a garantire che i blocchi esistano quando dovrebbero essere invocati.
  4. Dobbiamo assicurarci che DDBlockActionWrappersvengano ripuliti quando l'oggetto viene distrutto, quindi stiamo eseguendo un brutto trucco di swizzling -[UIControl dealloc]con uno nuovo che rimuove l'oggetto associato e quindi invoca il dealloccodice originale . Ingannevole, complicato. In realtà, gli oggetti associati vengono puliti automaticamente durante la deallocazione .

Infine, questo codice è stato digitato nel browser e non è stato compilato. Probabilmente ci sono alcune cose che non vanno. Il tuo chilometraggio può variare.


4
Si noti che ora è possibile utilizzare objc_implementationWithBlock()e class_addMethod()per risolvere questo problema in modo leggermente più efficiente rispetto all'utilizzo di oggetti associati (il che implica una ricerca hash che non è efficiente come la ricerca del metodo). Probabilmente una differenza di prestazioni irrilevante, ma è un'alternativa.
bbum

@bbum intendi imp_implementationWithBlock?
vikingosegundo

Sì, quello. Una volta era stato chiamato objc_implementationWithBlock(). :)
bbum

L'utilizzo di questo per i pulsanti nelle personalizzazioni UITableViewCellrisulterà nella duplicazione delle azioni-obiettivi desiderate poiché ogni nuovo obiettivo è una nuova istanza e quelli precedenti non vengono ripuliti per gli stessi eventi. Devi prima pulire gli obiettivi for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene

Penso che una cosa che renda il codice sopra più chiaro è sapere che un UIControl può accettare molti target: coppie di azioni .. da qui la necessità di creare un array mutabile per memorizzare tutte quelle coppie
abbood

41

I blocchi sono oggetti. Passa il tuo blocco come targetargomento, con @selector(invoke)come actionargomento, in questo modo:

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

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

Interessante. Cercherò di vedere se riesco a fare qualcosa di simile stasera. Può iniziare una nuova domanda.
Tad Donaghe

31
Questo "funziona" per coincidenza. Si basa su API private; il invokemetodo sugli oggetti Block non è pubblico e non deve essere utilizzato in questo modo.
bbum

1
Bbum: Hai ragione. Avevo pensato che -invoke fosse pubblico, ma avevo intenzione di aggiornare la mia risposta e segnalare un bug.
lemnar

1
sembra una soluzione fantastica, ma mi chiedo se sia accettabile da Apple in quanto utilizza un'API privata.
Brian

1
Funziona quando è passato nilinvece di @selector(invoke).
k06a

17

No, selettori e blocchi non sono tipi compatibili in Objective-C (in effetti, sono cose molto diverse). Dovrai scrivere il tuo metodo e passare invece il suo selettore.


11
In particolare, un selettore non è qualcosa che esegui; è il nome del messaggio che invii a un oggetto (o che un altro oggetto invii a un terzo oggetto, come in questo caso: stai dicendo al controllo di inviare un messaggio [il selettore va qui] alla destinazione). Un blocco, d'altra parte, è qualcosa che esegui: chiami il blocco direttamente, indipendentemente da un oggetto.
Peter Hosey

7

È possibile passare un blocco Objective-C per l'argomento @selector in un UIButton?

Prendendo tutte le risposte già fornite, la risposta è Sì, ma è necessario un po 'di lavoro per impostare alcune categorie.

Consiglio di usare NSInvocation perché puoi fare molto con questo, ad esempio con i timer, memorizzati come oggetto e invocati ... ecc ...

Ecco cosa ho fatto, ma nota che sto usando ARC.

La prima è una semplice categoria su 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

La prossima è una categoria su NSInvocation da memorizzare in un blocco:

.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

Ecco come usarlo:

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

Puoi fare molto con l'invocazione e i metodi Objective-C standard. Ad esempio, puoi utilizzare NSInvocationOperation (initWithInvocation :), NSTimer (chedTimerWithTimeInterval: invocation: repeates :)

Il punto è trasformare il tuo blocco in un NSInvocation è più versatile e può essere utilizzato come tale:

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

Ancora una volta questo è solo un suggerimento.


Un'altra cosa, invocare qui è un metodo pubblico. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin,

5

Non così semplice, sfortunatamente.

In teoria, sarebbe possibile definire una funzione che aggiunge dinamicamente un metodo alla classe di target, fare in modo che quel metodo esegua il contenuto di un blocco e restituisca un selettore secondo le necessità actiondell'argomento. Questa funzione potrebbe utilizzare la tecnica utilizzata da MABlockClosure , che, nel caso di iOS, dipende da un'implementazione personalizzata di libffi, che è ancora sperimentale.

È meglio implementare l'azione come metodo.


4

La libreria BlocksKit su Github (disponibile anche come CocoaPod) ha questa funzionalità integrata.

Dai un'occhiata al file di intestazione per UIControl + BlocksKit.h. Hanno implementato l'idea di Dave DeLong, quindi non devi farlo tu. Alcuni documenti sono qui .


1

Qualcuno mi dirà perché è sbagliato, forse, o con un po 'di fortuna, forse no, quindi o imparerò qualcosa o sarò d'aiuto.

L'ho appena messo insieme. È davvero semplice, solo un involucro sottile con un po 'di casting. Un avvertimento, si presume che il blocco che stai invocando abbia la firma corretta per corrispondere al selettore che usi (cioè numero di argomenti e tipi).

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

Non c'è davvero niente di magico in corso. Solo un sacco di downcasting void *e typecasting su una firma di blocco utilizzabile prima di invocare il metodo. Ovviamente (proprio come con performSelector:e metodo associato, le possibili combinazioni di input sono finite, ma estendibili se si modifica il codice.

Usato in questo modo:

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

Emette:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Il blocco è stato richiamato con str = Test

Utilizzato in uno scenario di azione mirata, devi solo fare qualcosa del genere:

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

Poiché il target in un sistema di azione target non viene mantenuto, sarà necessario assicurarsi che l'oggetto di chiamata sia in vita per tutto il tempo in cui dura il controllo.

Mi interessa sentire qualcosa da qualcuno più esperto di me.


hai una perdita di memoria in quello scenario di azione di destinazione perché invocationnon viene mai rilasciato
user102008

1

Avevo bisogno di un'azione associata a un UIButton all'interno di un UITableViewCell. Volevo evitare di utilizzare i tag per rintracciare ogni pulsante in ogni cella diversa. Ho pensato che il modo più diretto per ottenere ciò fosse associare un '"azione" di blocco al pulsante in questo modo:

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

La mia implementazione è un po 'più semplificata, grazie a @bbum per la menzione imp_implementationWithBlocke class_addMethod, (sebbene non ampiamente testata):

#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

Non funziona avere un NSBlockOperation (iOS SDK +5). Questo codice utilizza ARC ed è una semplificazione di un'app con cui lo sto testando (sembra funzionare, almeno apparentemente, non sono sicuro se sto perdendo 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];
}

Naturalmente, non sono sicuro di quanto sia buono per un utilizzo reale. Devi mantenere vivo un riferimento a NSBlockOperation o penso che ARC lo ucciderà.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.