Kann ich mit Objective-C einen Block als @selector übergeben?

90

Ist es möglich, einen Objective-C-Block für das @selectorArgument in a zu übergeben UIButton? dh Gibt es eine Möglichkeit, Folgendes zum Laufen zu bringen?

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

Vielen Dank

Bill Shiff
quelle

Antworten:

69

Ja, aber Sie müssten eine Kategorie verwenden.

Etwas wie:

@interface UIControl (DDBlockActions)

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

@end

Die Implementierung wäre etwas kniffliger:

#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

Einige Erklärungen:

  1. Wir verwenden eine benutzerdefinierte "nur interne" Klasse namens DDBlockActionWrapper. Dies ist eine einfache Klasse mit einer Blockeigenschaft (dem Block, den wir aufrufen möchten) und einer Methode, die diesen Block einfach aufruft.
  2. Die UIControlKategorie instanziiert einfach einen dieser Wrapper, gibt ihm den aufzurufenden Block und fordert sich dann auf, diesen Wrapper und seine invokeBlock:Methode als Ziel und Aktion (wie normal) zu verwenden.
  3. Die UIControlKategorie verwendet ein zugeordnetes Objekt zum Speichern eines Arrays von DDBlockActionWrappers, da UIControldie Ziele nicht beibehalten werden. Dieses Array soll sicherstellen, dass die Blöcke vorhanden sind, wenn sie aufgerufen werden sollen.
  4. Wir müssen sicherstellen , dass die DDBlockActionWrappersget gereinigt, wenn das Objekt zerstört wird, so dass wir einen fiesen Hack von Swizzling heraus tun -[UIControl dealloc]mit einer neuen, der das zugehörige Objekt entfernt, und ruft dann den ursprünglichen deallocCode. Tricky, tricky. Tatsächlich werden zugeordnete Objekte während der Freigabe automatisch bereinigt .

Schließlich wurde dieser Code im Browser eingegeben und nicht kompiliert. Es sind wahrscheinlich einige Dinge falsch daran. Ihr Kilometerstand kann variieren.

Dave DeLong
quelle
4
Beachten Sie, dass Sie dieses Problem jetzt etwas effizienter verwenden objc_implementationWithBlock()und class_addMethod()lösen können als zugehörige Objekte (was eine Hash-Suche impliziert, die nicht so effizient ist wie die Methodensuche). Wahrscheinlich ein irrelevanter Leistungsunterschied, aber es ist eine Alternative.
bbum
@bbum meinst du imp_implementationWithBlock?
Vikingosegundo
Ja - das hier. Es wurde einmal benannt objc_implementationWithBlock(). :)
bbum
Die Verwendung dieser Schaltfläche für benutzerdefinierte Schaltflächen UITableViewCellführt zu einer Verdoppelung der gewünschten Zielaktionen, da jedes neue Ziel eine neue Instanz ist und die vorherigen nicht für dieselben Ereignisse bereinigt werden. Sie müssen zuerst die Ziele reinigen for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Eugene
Ich denke, eine Sache, die den obigen Code klarer macht, ist zu wissen, dass ein UIControl viele Ziel-Aktionspaare akzeptieren kann. Daher muss ein veränderliches Array erstellt werden, um alle diese Paare zu speichern
Abbood
41

Blöcke sind Objekte. Übergeben Sie Ihren Block als targetArgument und @selector(invoke)als actionArgument wie folgt:

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

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];
Lemnar
quelle
Das ist interessant. Ich werde sehen, ob ich heute Abend etwas Ähnliches tun kann. Kann eine neue Frage beginnen.
Tad Donaghe
31
Dies "funktioniert" zufällig. Es basiert auf einer privaten API. Die invokeMethode für Blockobjekte ist nicht öffentlich und soll nicht auf diese Weise verwendet werden.
bbum
1
Bbum: Du hast recht. Ich hatte gedacht, -invoke sei öffentlich, aber ich wollte meine Antwort aktualisieren und einen Fehler melden.
Lemnar
1
Es scheint eine großartige Lösung zu sein, aber ich frage mich, ob es für Apple akzeptabel ist, da es eine private API verwendet.
Brian
1
Funktioniert wenn übergeben nilstatt @selector(invoke).
k06a
17

Nein, Selektoren und Blöcke sind in Objective-C keine kompatiblen Typen (tatsächlich sind sie sehr unterschiedliche Dinge). Sie müssen Ihre eigene Methode schreiben und stattdessen deren Selektor übergeben.

BoltClock
quelle
11
Insbesondere führen Sie einen Selektor nicht aus. Dies ist der Name der Nachricht, die Sie an ein Objekt senden (oder die ein anderes Objekt an ein drittes Objekt senden lässt, wie in diesem Fall: Sie weisen das Steuerelement an, eine Nachricht [Auswahl geht hierher] an das Ziel zu senden). Ein Block hingegen wird von Ihnen ausgeführt: Sie rufen den Block unabhängig von einem Objekt direkt auf.
Peter Hosey
7

Ist es möglich, einen Objective-C-Block für das @ selector-Argument in einem UIButton zu übergeben?

Wenn Sie alle bereits bereitgestellten Antworten berücksichtigen, lautet die Antwort Ja, aber ein wenig Arbeit ist erforderlich, um einige Kategorien einzurichten.

Ich empfehle die Verwendung von NSInvocation, da Sie damit viel tun können, z. B. mit Timern, die als Objekt gespeichert und aufgerufen werden ... etc ...

Hier ist, was ich getan habe, aber beachten Sie, dass ich ARC verwende.

Erstens ist eine einfache Kategorie auf 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

Als nächstes folgt eine Kategorie in NSInvocation, die in einem Block gespeichert werden soll:

.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

So verwenden Sie es:

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

Mit dem Aufruf und den Standard-Objective-C-Methoden können Sie viel tun. Beispielsweise können Sie NSInvocationOperation (initWithInvocation :), NSTimer (ScheduledTimerWithTimeInterval: Aufruf: Wiederholungen :) verwenden.

Der Punkt ist, Ihren Block in eine NSInvocation zu verwandeln, ist vielseitiger und kann als solche verwendet werden:

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

Auch dies ist nur ein Vorschlag.

Arvin
quelle
Eine weitere Sache, die hier aufgerufen wird, ist eine öffentliche Methode. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin
5

Leider nicht so einfach.

Theoretisch wäre es möglich, eine Funktion zu definieren, die der Klasse von dynamisch eine Methode hinzufügt target, diese Methode den Inhalt eines Blocks ausführen lässt und einen Selektor zurückgibt, der vom actionArgument benötigt wird . Diese Funktion könnte die von MABlockClosure verwendete Technik verwenden , die im Fall von iOS von einer benutzerdefinierten Implementierung von libffi abhängt, die noch experimentell ist.

Sie sollten die Aktion besser als Methode implementieren.

Quinn Taylor
quelle
4

In der Bibliothek BlocksKit auf Github (auch als CocoaPod erhältlich) ist diese Funktion integriert.

Schauen Sie sich die Header-Datei für UIControl + BlocksKit.h an. Sie haben Dave DeLongs Idee umgesetzt, damit Sie es nicht müssen. Einige Dokumentationen finden Sie hier .

Nate Cook
quelle
1

Jemand wird mir sagen, warum das vielleicht falsch ist, oder mit etwas Glück, vielleicht auch nicht, also werde ich entweder etwas lernen oder ich werde hilfreich sein.

Ich habe das einfach zusammengeschmissen. Es ist wirklich einfach, nur eine dünne Hülle mit ein bisschen Casting. Ein Wort der Warnung: Es wird davon ausgegangen, dass der von Ihnen aufgerufene Block die richtige Signatur hat, die dem von Ihnen verwendeten Selektor entspricht (dh Anzahl der Argumente und Typen).

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

Und

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

Es ist wirklich nichts Magisches los. Nur viel Downcasting void *und Typografie auf eine verwendbare Blocksignatur, bevor die Methode aufgerufen wird. Offensichtlich (genau wie bei performSelector:und der zugehörigen Methode sind die möglichen Kombinationen von Eingaben endlich, aber erweiterbar, wenn Sie den Code ändern.

So verwendet:

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

Es gibt aus:

2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Block wurde mit str = Test aufgerufen

In einem Zielaktionsszenario müssen Sie nur Folgendes tun:

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

Da das Ziel in einem Zielaktionssystem nicht beibehalten wird, müssen Sie sicherstellen, dass das Aufrufobjekt so lange lebt, wie es das Steuerelement selbst tut.

Ich bin daran interessiert, etwas von jemandem zu hören, der erfahrener ist als ich.

d11wtq
quelle
Sie haben einen Speicherverlust in diesem invocation
Zielaktionsszenario,
1

Ich brauchte eine Aktion, die einem UIButton in einer UITableViewCell zugeordnet war. Ich wollte vermeiden, Tags zu verwenden, um jede Schaltfläche in jeder anderen Zelle aufzuspüren. Ich dachte, der direkteste Weg, dies zu erreichen, wäre, der Schaltfläche eine Block- "Aktion" wie folgt zuzuordnen:

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

Meine Implementierung ist etwas einfacher, dank @bbum für die Erwähnung imp_implementationWithBlockund class_addMethod(obwohl nicht ausführlich getestet):

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

Funktioniert es nicht, eine NSBlockOperation (iOS SDK +5) zu haben? Dieser Code verwendet ARC und ist eine Vereinfachung einer App, mit der ich dies teste (scheint zumindest anscheinend zu funktionieren, nicht sicher, ob mir Speicherplatz ausgeht).

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

Natürlich bin ich mir nicht sicher, wie gut dies für den tatsächlichen Gebrauch ist. Sie müssen einen Verweis auf die NSBlockOperation am Leben erhalten, sonst denke ich, dass ARC sie töten wird.

Rufo
quelle