Wie verwende ich performSelector: withObject: afterDelay: mit primitiven Typen in Cocoa?

97

Mit dieser NSObjectMethode performSelector:withObject:afterDelay:kann ich nach einer bestimmten Zeit eine Methode für das Objekt mit einem Objektargument aufrufen. Es kann nicht für Methoden mit einem Nicht-Objekt-Argument verwendet werden (z. B. Ints, Floats, Strukturen, Nicht-Objekt-Zeiger usw.).

Was ist der einfachste Weg, um dasselbe mit einer Methode mit einem Nicht-Objekt-Argument zu erreichen? Ich weiß, dass für reguläre performSelector:withObject:die Lösung zu verwenden ist NSInvocation(was übrigens wirklich kompliziert ist). Aber ich weiß nicht, wie ich mit dem Teil "Verzögerung" umgehen soll.

Vielen Dank,

user102008
quelle
Es ist ein bisschen hackisch, aber ich finde es nützlich, schnellen Code zu schreiben. So etwas wie: id object = [array performSelector: @selector (objectAtIndex :) withObject: (__bridge_transfer) (void *) 1]; . Wenn das Argument 0 ist, benötigen Sie nicht einmal eine Brückenbesetzung, da 0 "speziell" ist und die ID damit kompatibel ist.
Ramy Al Zuhouri

Antworten:

72

Folgendes habe ich verwendet, um etwas zu nennen, das ich mit NSInvocation nicht ändern konnte:

SEL theSelector = NSSelectorFromString(@"setOrientation:animated:");
NSInvocation *anInvocation = [NSInvocation
            invocationWithMethodSignature:
            [MPMoviePlayerController instanceMethodSignatureForSelector:theSelector]];

[anInvocation setSelector:theSelector];
[anInvocation setTarget:theMovie];
UIInterfaceOrientation val = UIInterfaceOrientationPortrait;
BOOL anim = NO;
[anInvocation setArgument:&val atIndex:2];
[anInvocation setArgument:&anim atIndex:3];

[anInvocation performSelector:@selector(invoke) withObject:nil afterDelay:1];
Morty
quelle
Warum nicht einfach tun [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? Oder meinen Sie, dass sich die invokeNachricht in der Methode befindet, die Sie nach der Verzögerung ausführen?
Peter Hosey
Ah ya, ich habe vergessen zu sagen .. `[anInvocation performSelector: @selector (aufrufen) afterDelay: 0.5];
Morty
24
Nur eine Randnotiz für diejenigen, die es nicht wissen: Wir fügen Argumente aus Index 2 hinzu, da Index 0 und 1 für versteckte Argumente "self" und "_cmd" reserviert sind.
Vishal Singh
35

Wickeln Sie einfach float, boolean, int oder ähnliches in eine NSNumber.

Für Strukturen kenne ich keine praktische Lösung, aber Sie könnten eine separate ObjC-Klasse erstellen, die eine solche Struktur besitzt.

schadet
quelle
5
Erstellen Sie eine Wrapper-Methode, -delayedMethodWithArgument: (NSNumber *) arg. Lassen Sie es die ursprüngliche Methode aufrufen, nachdem Sie das Grundelement aus der NSNumber gezogen haben.
James Williams
1
Verstanden. Dann bin ich mir einer eleganten Lösung nicht sicher, aber Sie könnten eine andere Methode hinzufügen, die als Ziel Ihres performSelector gedacht ist:, die die NSNumber entpackt und den endgültigen Selektor für die Methode ausführt, an die Sie gerade denken. Eine andere Idee, die Sie untersuchen könnten, besteht darin, eine Kategorie in NSObject zu erstellen, die perforSelector: withInt: ... (und ähnliches) hinzufügt.
schadet
32
NSValue (die Superklasse von NSNumber) verpackt alles, was Sie ihm geben. Wenn Sie eine Instanz von 'struct foo' mit dem Namen 'bar' umbrechen möchten, tun Sie '[NSValue valueWithBytes: & bar objCType: @encode (struct foo)];'
Jim Dovey
2
Sie können NSValue verwenden, um eine Struktur zu verpacken. In jedem Fall ist NSNumber / NSValue die richtige Lösung.
Peter Hosey
6
Bestätigt: MUSS übergeben, nildamit ein BOOLParameter empfangen wird NO( FALSE)
Nicolas Miari
13

VERWENDEN SIE DIESE ANTWORT NICHT. Ich habe es nur für historische Zwecke verlassen. SIEHE DIE KOMMENTARE UNTEN.

Es gibt einen einfachen Trick, wenn es sich um einen BOOL-Parameter handelt.

Übergeben Sie Null für NEIN und Selbst für JA. nil wird auf den BOOL-Wert NO gesetzt. self wird auf den BOOL-Wert von YES gesetzt.

Dieser Ansatz bricht zusammen, wenn es sich nicht um einen BOOL-Parameter handelt.

Selbst anzunehmen ist ein UIView.

//nil will be cast to NO when the selector is performed
[self performSelector:@selector(setHidden:) withObject:nil afterDelay:5.0];

//self will be cast to YES when the selector is performed
[self performSelector:@selector(setHidden:) withObject:self afterDelay:10.0];

quelle
Ich glaube, dass BOOL-Werte immer 0 oder 1 sein sollten. Es ist wahr, dass es den meisten Codes egal ist und sie einfach if ()testen, aber es ist denkbar, dass einige Codes davon abhängen, dass sie 0 oder 1 sind und somit gewonnen werden Behandeln Sie einen großen Adresswert nicht als gleich 1 (JA).
user102008
1
Vielen Dank dafür, ich wollte keinen Wrapper erstellen oder hässliche GCD-Syntax schreiben, um das zu tun.
0xSina
10
BENUTZEN SIE ES NICHT . Dieser Ansatz ist die Quelle schwerwiegender Fehler, die schwer zu finden sind. Stellen Sie sich selfmit Adresse wie vor 0x0123400. Mit diesem Ansatz erhalten Sie in 0,4% der Fälle KEIN JA . Worser, mit dieser Wahrscheinlichkeit kann diese Lösung alle Tests bestehen und später enthüllen.
Oleg Trakhman
1
UPD: Man würde mit 6% Wahrscheinlichkeit wegen der Speicherausrichtung KEIN JA erhalten .
Oleg Trakhman
4
@ iVishal
BOOL an
8

Vielleicht NSValue , stellen Sie einfach sicher, dass Ihre Zeiger nach der Verzögerung noch gültig sind (dh keine auf dem Stapel zugewiesenen Objekte).

Remus Rusanu
quelle
Wird eine alte Methode NSValue(wenn möglich) die erwartete "entpacken" type?
Alex Gray
8

Ich weiß, dass dies eine alte Frage ist, aber wenn Sie iOS SDK 4+ erstellen, können Sie Blöcke verwenden, um dies mit sehr geringem Aufwand zu tun und es lesbarer zu machen:

double delayInSeconds = 2.0;
int primitiveValue = 500;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self doSomethingWithPrimitive:primitiveValue];     
});
Michael Gaylord
quelle
Dies erzeugt eine Retain-Schleife. Damit dies richtig funktioniert, müssen Sie eine schwache Referenz auf sich selbst erstellen und diese Referenz verwenden, um den Selektor auszuführen. "__block typeof (self) schwachSelf = self;"
Tim Wachter
1
Sie benötigen hier keine schwache Referenz, da self den Block nicht beibehält (es gibt keine Retain-Schleife). Es ist wahrscheinlich vorzuziehen, eine starke Referenz zu verwenden, da das Selbst andernfalls freigegeben werden könnte. Siehe: stackoverflow.com/a/19018633/251687
Sebastien Martin
6

PerformSelector: WithObject nimmt immer ein Objekt, um Argumente wie int / double / float usw. zu übergeben. Sie können so etwas verwenden.

//NSNumber is an object..

    [self performSelector:@selector(setUserAlphaNumber:) withObject: [NSNumber numberWithFloat: 1.0f]
    afterDelay:1.5];

    -(void) setUserAlphaNumber: (NSNumber*) number{

     [txtUsername setAlpha: [number floatValue] ];
    }

Genauso können Sie [NSNumber numberWithInt:] etc .... verwenden und bei der Empfangsmethode die Nummer in Ihr Format als [number int] oder [number double] konvertieren.

Augustinus
quelle
1
Wenn man auf + performSelector: withObject: + beschränkt ist, ist dies die einzig richtige Antwort, IMO. Aber @ MichaelGaylords Antwort mit Blöcken ist sauberer, wenn das eine Option für Sie ist.
big_m
5

Blöcke sind der richtige Weg. Sie können komplexe Parameter haben, Sicherheit eingeben und es ist viel einfacher und sicherer als die meisten alten Antworten hier. Zum Beispiel könnten Sie einfach schreiben:

[MONBlock performBlock:^{[obj setFrame:SOMETHING];} afterDelay:2];

Mit Blöcken können Sie beliebige Parameterlisten, Referenzobjekte und Variablen erfassen.

Backing-Implementierung (grundlegend):

@interface MONBlock : NSObject

+ (void)performBlock:(void(^)())pBlock afterDelay:(NSTimeInterval)pDelay;

@end

@implementation MONBlock

+ (void)imp_performBlock:(void(^)())pBlock
{
 pBlock();
}

+ (void)performBlock:(void(^)())pBlock afterDelay:(NSTimeInterval)pDelay
{
  [self performSelector:@selector(imp_performBlock:)
             withObject:[pBlock copy]
             afterDelay:pDelay];
}

@end

Beispiel:

int main(int argc, const char * argv[])
{
 @autoreleasepool {
  __block bool didPrint = false;
  int pi = 3; // close enough =p

  [MONBlock performBlock:^{NSLog(@"Hello, World! pi is %i", pi); didPrint = true;} afterDelay:2];

  while (!didPrint) {
   [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeInterval:0.1 sinceDate:NSDate.date]];
  }

  NSLog(@"(Bye, World!)");
 }
 return 0;
}

Siehe auch Michaels Antwort (+1) für ein anderes Beispiel.

Justin
quelle
3

Ich würde immer empfehlen, dass Sie NSMutableArray als Weitergabeobjekt verwenden. Dies liegt daran, dass Sie dann mehrere Objekte übergeben können, z. B. die gedrückte Taste und andere Werte. NSNumber, NSInteger und NSString sind nur Container mit einem gewissen Wert. Stellen Sie sicher, dass Sie beim Abrufen des Objekts aus dem Array auf einen korrekten Containertyp verweisen. Sie müssen NS-Container weitergeben. Dort können Sie den Wert testen. Denken Sie daran, dass Container beim Vergleich von Werten isEqual verwenden.

#define DELAY_TIME 5

-(void)changePlayerGameOnes:(UIButton*)sender{
    NSNumber *nextPlayer = [NSNumber numberWithInt:[gdata.currentPlayer intValue]+1 ];
    NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:sender, nil];
    [array addObject:nextPlayer];
    [self performSelector:@selector(next:) withObject:array afterDelay:DELAY_TIME];
}
-(void)next:(NSMutableArray*)nextPlayer{
    if(gdata != nil){ //if game choose next player
       [self nextPlayer:[nextPlayer objectAtIndex:1] button:[nextPlayer objectAtIndex:0]];
    }
}
Odd-Arne
quelle
2

Ich wollte dies auch tun, aber mit einer Methode, die einen BOOL-Parameter empfängt. Umschließen des Bool-Werts mit NSNumber, Fehler beim Übergeben des Werts. Ich habe keine Idee warum.

Also habe ich einen einfachen Hack gemacht. Ich füge den erforderlichen Parameter in eine andere Dummy-Funktion ein und rufe diese Funktion mit dem performSelector auf, wobei withObject = nil;

[self performSelector:@selector(dummyCaller:) withObject:nil afterDelay:5.0];

-(void)dummyCaller {

[self myFunction:YES];

}
GeneCode
quelle
Siehe meinen Kommentar oben zur Antwort von harms. Da die -performSelector[...]Methode ein Objekt erwartet (anstelle eines primitiven Datentyps) und nicht weiß, dass der aufgerufene Selektor einen Booleschen Wert akzeptiert, wird die Adresse (Zeigerwert) der NSNumberInstanz möglicherweise blind in BOOL(= umgewandelt ungleich Null, dh TRUE). Vielleicht könnte die Laufzeit schlauer sein als diese und einen Null-Booleschen Wert erkennen, der in ein NSNumber!
Nicolas Miari
Genau. Ich denke, eine bessere Sache ist es, BOOL insgesamt zu vermeiden und die Ganzzahl 0 für NEIN und 1 für JA zu verwenden und diese mit NSNumber oder NSValue zu verpacken.
GeneCode
Ändert das etwas? Wenn ja, bin ich plötzlich wirklich enttäuscht von Cocoa :)
Nicolas Miari
2

Ich finde, dass der schnellste (aber etwas schmutzige) Weg, dies zu tun, darin besteht, objc_msgSend direkt aufzurufen. Es ist jedoch gefährlich, es direkt aufzurufen, da Sie die Dokumentation lesen und sicherstellen müssen, dass Sie die richtige Variante für den Typ des Rückgabewerts verwenden, und weil objc_msgSend aus Gründen des Compiler-Komforts als vararg definiert ist, aber tatsächlich als schneller Montagekleber implementiert ist . Hier ist ein Code zum Aufrufen einer Delegate-Methode - [delegate integerDidChange:], die ein einzelnes Integer-Argument akzeptiert.

#import <objc/message.h>


SEL theSelector = @selector(integerDidChange:);
if ([self.delegate respondsToSelector:theSelector])
{
    typedef void (*IntegerDidChangeFuncPtrType)(id, SEL, NSInteger);
    IntegerDidChangeFuncPtrType MyFunction = (IntegerDidChangeFuncPtrType)objc_msgSend;
    MyFunction(self.delegate, theSelector, theIntegerThatChanged);
}

Dadurch wird zuerst der Selektor gespeichert, da wir mehrmals darauf verweisen werden und es einfach wäre, einen Tippfehler zu erstellen. Anschließend wird überprüft, ob der Delegat tatsächlich auf den Selektor reagiert. Dies kann ein optionales Protokoll sein. Anschließend wird ein Funktionszeigertyp erstellt, der die tatsächliche Signatur des Selektors angibt. Beachten Sie, dass alle Objective-C-Nachrichten zwei versteckte erste Argumente haben, nämlich das zu sendende Objekt und die gesendete Auswahl. Dann erstellen wir einen Funktionszeiger des entsprechenden Typs und setzen ihn so, dass er auf die zugrunde liegende Funktion objc_msgSend zeigt. Beachten Sie, dass Sie eine andere Variante von objc_msgSend verwenden müssen, wenn der Rückgabewert ein float oder eine Struktur ist. Senden Sie die Nachricht abschließend mit derselben Maschine, die Objective-C unter den Arbeitsblättern verwendet.

Jason Harris
quelle
"Denken Sie daran, dass, wenn Sie Floats oder Strukturen senden ..." Sie meinen , Floats oder Strukturen zurückzugeben
user102008
Diese drei Zeilen geben Ihnen Typensicherheit und garantieren, dass die Anzahl der Argumente Ihren Erwartungen entspricht. objc_msgSend ist eine vararg-Funktion, die tatsächlich als Baugruppenkleber implementiert ist. Wenn Sie also nicht typisieren, können Sie ihr alles geben.
Jason Harris
1

Sie können einfach NSTimer verwenden, um einen Selektor aufzurufen:

[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(yourMethod:) userInfo:nil repeats:NO]
Unis
quelle
Ihnen fehlt das Argument. Das Argument yourMethod:in Ihrem Beispiel ist der Timer selbst, für den Sie nichts übergeben haben userInfo. Selbst wenn Sie verwendet hätten userInfo, müssten Sie den Wert immer noch einpacken, wie es der Schaden und Remus Rusanu vorschlugen.
Peter Hosey
-1 für die Nichtverwendung von performSelector und das Erstellen einer nicht erforderlichen NSTimer-Instanz
Applicasa iOS-Entwickler
1

Das Aufrufen von performSelector mit einer NSNumber oder einem anderen NSValue funktioniert nicht. Anstatt den Wert von NSValue / NSNumber zu verwenden, wird der Zeiger effektiv auf ein int, float oder was auch immer umgewandelt und verwendet.

Die Lösung ist jedoch einfach und offensichtlich. Erstellen Sie die NSInvocation und rufen Sie an

[invocation performSelector:@selector(invoke) withObject:nil afterDelay:delay]

Anomie
quelle
0

Pehaps ... ok, sehr wahrscheinlich fehlt mir etwas, aber warum nicht einfach einen Objekttyp, z. B. NSNumber, als Container für Ihre Nicht-Objekttyp-Variable wie CGFloat erstellen?

CGFloat myFloat = 2.0; 
NSNumber *myNumber = [NSNumber numberWithFloat:myFloat];

[self performSelector:@selector(MyCalculatorMethod:) withObject:myNumber afterDelay:5.0];
Jim Hillhouse
quelle
Die Frage betrifft die Übergabe primitiver Typen, nicht von NSNumber-Objekten.
Rudolf Adamkovič
Die Frage setzt voraus, dass das OP nur externen Zugriff auf die Methode hat und die Signatur nicht ändern kann.
Alex Gray