Puis-je passer un bloc en tant que @selector avec Objective-C?

90

Est-il possible de passer un bloc Objective-C pour l' @selectorargument dans un UIButton? Existe-t-il un moyen de faire fonctionner les éléments suivants?

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

Merci

Bill Shiff
la source

Réponses:

69

Oui, mais vous devrez utiliser une catégorie.

Quelque chose comme:

@interface UIControl (DDBlockActions)

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

@end

La mise en œuvre serait un peu plus délicate:

#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

Quelques explications:

  1. Nous utilisons une classe personnalisée "interne uniquement" appelée DDBlockActionWrapper. Il s'agit d'une classe simple qui a une propriété de bloc (le bloc que nous voulons invoquer) et une méthode qui appelle simplement ce bloc.
  2. La UIControlcatégorie instancie simplement l'un de ces wrappers, lui donne le bloc à invoquer, puis se dit d'utiliser ce wrapper et sesinvokeBlock: méthode comme cible et action (comme d'habitude).
  3. La UIControlcatégorie utilise un objet associé pour stocker un tableau de DDBlockActionWrappers, car UIControlne conserve pas ses cibles. Ce tableau permet de garantir que les blocs existent lorsqu'ils sont censés être appelés.
  4. Nous devons nous assurer que le DDBlockActionWrappersnettoyage est effectué lorsque l'objet est détruit, nous faisons donc un vilain piratage de swizzling -[UIControl dealloc]avec un nouveau qui supprime l'objet associé, puis invoque le dealloccode d' origine . Tricky, délicat. En fait, les objets associés sont nettoyés automatiquement lors de la désallocation .

Enfin, ce code a été tapé dans le navigateur et n'a pas été compilé. Il y a probablement des problèmes avec cela. Votre kilométrage peut varier.

Dave DeLong
la source
4
Notez que vous pouvez maintenant utiliser objc_implementationWithBlock()et class_addMethod()résoudre ce problème de manière légèrement plus efficace que d'utiliser des objets associés (ce qui implique une recherche de hachage qui n'est pas aussi efficace que la recherche de méthode). Probablement une différence de performance non pertinente, mais c'est une alternative.
bbum
@bbum voulez-vous dire imp_implementationWithBlock?
vikingosegundo du
Ouais - celui-là. Il a été nommé une fois objc_implementationWithBlock(). :)
bbum
L 'utilisation de ceci pour les boutons dans les personnalisations UITableViewCellentraînera la duplication des actions cibles souhaitées puisque chaque nouvelle cible est une nouvelle instance et que les précédentes ne sont pas nettoyées pour les mêmes événements. Vous devez d'abord nettoyer les cibles for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene
Je pense qu'une chose qui rend le code ci-dessus plus clair est de savoir qu'un UIControl peut accepter de nombreuses paires cible: action .. d'où la nécessité de créer un tableau mutable pour stocker toutes ces paires
abbood
41

Les blocs sont des objets. Passez votre bloc comme targetargument, avec @selector(invoke)comme actionargument, comme ceci:

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

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];
lemnar
la source
C'est intéressant. Je vais voir si je peux faire quelque chose de similaire ce soir. Peut commencer une nouvelle question.
Tad Donaghe
31
Cela "fonctionne" par hasard. Il repose sur une API privée; la invokeméthode sur les objets Block n'est pas publique et n'est pas destinée à être utilisée de cette manière.
bbum
1
Bbum: Vous avez raison. J'avais pensé que -invoke était public, mais j'avais l'intention de mettre à jour ma réponse et de signaler un bogue.
lemnar
1
cela semble être une solution géniale, mais je me demande si c'est acceptable par Apple car il utilise une API privée.
Brian
1
Fonctionne lorsqu'il est passé nilau lieu de @selector(invoke).
k06a
17

Non, les sélecteurs et les blocs ne sont pas des types compatibles en Objective-C (en fait, ce sont des choses très différentes). Vous devrez écrire votre propre méthode et passer son sélecteur à la place.

BoltClock
la source
11
En particulier, un sélecteur n'est pas quelque chose que vous exécutez; c'est le nom du message que vous envoyez à un objet (ou faites envoyer un autre objet à un troisième objet, comme dans ce cas: vous dites au contrôle d'envoyer un message [le sélecteur va ici] à la cible). Un bloc, par contre, est quelque chose que vous exécutez: vous appelez le bloc directement, indépendamment d'un objet.
Peter Hosey
7

Est-il possible de passer un bloc Objective-C pour l'argument @selector dans un UIButton?

En tenant compte de toutes les réponses déjà fournies, la réponse est Oui, mais un peu de travail est nécessaire pour configurer certaines catégories.

Je recommande d'utiliser NSInvocation car vous pouvez faire beaucoup avec cela, comme avec des minuteries, stockées en tant qu'objet et invoquées ... etc ...

Voici ce que j'ai fait, mais notez que j'utilise ARC.

Le premier est une catégorie simple sur 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

Vient ensuite une catégorie sur NSInvocation à stocker dans un bloc:

.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

Voici comment l'utiliser:

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

Vous pouvez faire beaucoup avec l'invocation et les méthodes Objective-C standard. Par exemple, vous pouvez utiliser NSInvocationOperation (initWithInvocation :), NSTimer (planifiéTimerWithTimeInterval: invocation: repeates :)

Le but est de transformer votre bloc en NSInvocation est plus polyvalent et peut être utilisé comme tel:

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

Encore une fois, ce n'est qu'une suggestion.

Arvin
la source
Une dernière chose, invoquer ici est une méthode publique. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin
5

Pas aussi simple que ça, malheureusement.

En théorie, il serait possible de définir une fonction qui ajoute dynamiquement une méthode à la classe de target, que cette méthode exécute le contenu d'un bloc et renvoie un sélecteur selon les besoins de l' actionargument. Cette fonction pourrait utiliser la technique utilisée par MABlockClosure , qui, dans le cas d'iOS, dépend d'une implémentation personnalisée de libffi, qui est encore expérimentale.

Il vaut mieux mettre en œuvre l'action en tant que méthode.

Quinn Taylor
la source
4

La bibliothèque BlocksKit sur Github (également disponible en CocoaPod) a cette fonctionnalité intégrée.

Jetez un œil au fichier d'en-tête pour UIControl + BlocksKit.h. Ils ont mis en œuvre l'idée de Dave DeLong pour que vous n'ayez pas à le faire. Une documentation est ici .

Nate Cook
la source
1

Quelqu'un va me dire pourquoi c'est faux, peut-être, ou avec un peu de chance, peut-être pas, alors j'apprendrai quelque chose ou je serai utile.

J'ai juste jeté ça ensemble. C'est vraiment basique, juste une fine enveloppe avec un peu de moulage. Un mot d'avertissement, cela suppose que le bloc que vous invoquez a la signature correcte pour correspondre au sélecteur que vous utilisez (c'est-à-dire le nombre d'arguments et de types).

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

Et

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

Il ne se passe vraiment rien de magique. Juste beaucoup de downcasting void *et de typage vers une signature de bloc utilisable avant d'appeler la méthode. Évidemment (tout comme avecperformSelector: et méthode associée, les combinaisons d'entrées possibles sont finies, mais extensibles si vous modifiez le code.

Utilisé comme ceci:

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

Il sort:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Le bloc a été appelé avec str = Test

Utilisé dans un scénario d'action cible, il vous suffit de faire quelque chose comme ceci:

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

Étant donné que la cible dans un système d'action-cible n'est pas conservée, vous devrez vous assurer que l'objet d'appel dure aussi longtemps que le contrôle lui-même.

Je suis intéressé d'entendre quelque chose de quelqu'un de plus expert que moi.

d11wtq
la source
vous avez une fuite de mémoire dans ce scénario d'action cible car il invocationn'est jamais publié
user102008
1

J'avais besoin d'avoir une action associée à un UIButton dans un UITableViewCell. Je voulais éviter d'utiliser des balises pour localiser chaque bouton dans chaque cellule différente. Je pensais que le moyen le plus direct pour y parvenir était d'associer un bloc "action" au bouton comme ceci:

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

Mon implémentation est un peu plus simplifiée, grâce à @bbum pour avoir mentionné imp_implementationWithBlocket class_addMethod, (bien que pas largement testé):

#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
la source
0

Cela ne fonctionne pas d'avoir une NSBlockOperation (iOS SDK +5). Ce code utilise ARC et c'est une simplification d'une application avec laquelle je teste cela (semble fonctionner, du moins apparemment, je ne sais pas si je fuit de la mémoire).

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

Bien sûr, je ne sais pas à quel point c'est bon pour un usage réel. Vous devez garder une référence à NSBlockOperation vivante ou je pense que l'ARC le tuera.

rufo
la source