Come utilizzare performSelector: withObject: afterDelay: con tipi primitivi in ​​Cocoa?


97

Il NSObjectmetodo performSelector:withObject:afterDelay:mi permette di invocare un metodo sull'oggetto con un argomento oggetto dopo un certo tempo. Non può essere utilizzato per metodi con un argomento non oggetto (ad esempio int, float, struct, puntatori non oggetto, ecc.).

Qual è il modo più semplice per ottenere la stessa cosa con un metodo con un argomento non oggetto? So che regolarmente performSelector:withObject:la soluzione è usare NSInvocation(che tra l'altro è davvero complicato). Ma non so come gestire la parte "ritardo".

Grazie,


È un po 'hacker ma trovo utile scrivere codice veloce. Qualcosa come: id object = [array performSelector: @selector (objectAtIndex :) withObject: (__bridge_transfer) (void *) 1]; . Se l'argomento è 0 non hai nemmeno bisogno di un cast di bridge perché 0 è "speciale" e id è compatibile con esso.
Ramy Al Zuhouri

Risposte:


72

Ecco come chiamavo qualcosa che non potevo cambiare usando NSInvocation:

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

Perché non farlo e basta [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? O vuoi dire che il invokemessaggio è nel metodo che esegui dopo il ritardo?
Peter Hosey

Ah ya, ho dimenticato di dire .. `[anInvocation performSelector: @selector (invoke) afterDelay: 0.5];
Morty

24
solo una nota a margine per coloro che non lo sanno, iniziamo ad aggiungere argomenti dall'indice 2 perché gli indici 0 e 1 sono riservati agli argomenti nascosti "self" e "_cmd".
Vishal Singh

35

Basta racchiudere il float, boolean, int o simili in un NSNumber.

Per le strutture, non conosco una soluzione pratica, ma potresti creare una classe ObjC separata che possiede una struttura di questo tipo.


5
Crea un metodo wrapper, -delayedMethodWithArgument: (NSNumber *) arg. Chiedi che chiami il metodo originale dopo aver estratto la primitiva da NSNumber.
James Williams

1
Inteso. Quindi non sono sicuro di una soluzione elegante, ma potresti aggiungere un altro metodo che è inteso come obiettivo del tuo performSelector :, che decomprime NSNumber ed esegue il selettore finale sul metodo che hai attualmente in mente. Un'altra idea che potresti esplorare è creare una categoria su NSObject che aggiunga perforSelector: withInt: ... (e simili).
danneggia

32
NSValue (la superclasse di NSNumber) racchiude tutto ciò che gli dai. Se vuoi racchiudere un'istanza di 'struct foo' chiamata 'bar', devi fare '[NSValue valueWithBytes: & bar objCType: @encode (struct foo)];'
Jim Dovey,

2
Puoi usare NSValue per racchiudere una struttura. In ogni caso, NSNumber / NSValue è la soluzione corretta.
Peter Hosey

6
Confermato: DEVE passare nilun BOOLparametro per ricevere NO( FALSE)
Nicolas Miari

13

NON USARE QUESTA RISPOSTA. L'HO LASCIATO SOLO PER SCOPI STORICI. VEDI I COMMENTI SOTTO.

C'è un semplice trucco se si tratta di un parametro BOOL.

Passa a zero per NO e auto per SÌ. nil viene convertito al valore BOOL di NO. self viene convertito al valore BOOL di YES.

Questo approccio si interrompe se è qualcosa di diverso da un parametro BOOL.

Supponendo che il sé sia ​​una visione UIV.

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

Credo che i valori BOOL dovrebbero sempre essere 0 o 1. È vero che alla maggior parte del codice non importa e farà semplicemente un if ()test su di esso, ma è concepibile che alcuni codici dipenderanno dal fatto che siano 0 o 1, e quindi vinti non considerare un valore di indirizzo di grandi dimensioni come uguale a 1 (YES).
user102008

1
Grazie per questo, non volevo creare un wrapper o scrivere una brutta sintassi di GCD per farlo.
0xSina

10
NON USARLO NESSUNO . Questo approccio è fonte di bug gravi, difficili da trovare. Immagina selfcon un indirizzo come 0x0123400. Con questo approccio, NON otterrai invece SÌ nello 0,4% dei casi. Peggio ancora, con questa probabilità, questa soluzione potrebbe superare tutti i test e rivelarsi in seguito.
Oleg Trakhman

1
UPD: si otterrebbe NO invece di SI con il 6% di probabilità a causa dell'allineamento della memoria.
Oleg Trakhman

4
@iVishal Dai un'occhiata agli spigoli vivi di BOOL
Farray

8

Forse NSValue , assicurati solo che i tuoi puntatori siano ancora validi dopo il ritardo (es. Nessun oggetto allocato nello stack).


Qualche vecchio metodo "unbox" NSValue(se possibile) è previsto type?
Alex Grey

8

So che questa è una vecchia domanda, ma se stai creando iOS SDK 4+, puoi utilizzare i blocchi per farlo con uno sforzo minimo e renderlo più leggibile:

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

Questo crea un loop di ritenzione. Affinché funzioni correttamente, è necessario fare un riferimento debole a sé e utilizzare quel riferimento per eseguire il selettore. "__block typeof (self) weakSelf = self;"
Tim Wachter

1
Non hai bisogno di un riferimento debole qui perché self non mantiene il blocco (non esiste un loop di conservazione). Probabilmente è preferibile utilizzare un riferimento forte in realtà poiché altrimenti self potrebbe essere deallocato. Vedi: stackoverflow.com/a/19018633/251687
Sebastien Martin

6

PerformSelector: WithObject prende sempre un oggetto, quindi per passare argomenti come int / double / float ecc ..... Puoi usare qualcosa di simile.

//NSNumber is an object..

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

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

     [txtUsername setAlpha: [number floatValue] ];
    }

Allo stesso modo puoi usare [NSNumber numberWithInt:] ecc .... e nel metodo di ricezione puoi convertire il numero nel tuo formato come [number int] o [number double].


1
Se uno è limitato a + performSelector: withObject: +, questa è l'unica risposta corretta pubblicata, IMO. Ma la risposta di @ MichaelGaylord usando i blocchi è più pulita, se questa è un'opzione per te.
big_m

5

I blocchi sono la strada da percorrere. Puoi avere parametri complessi, sicurezza dei tipi ed è molto più semplice e sicuro della maggior parte delle vecchie risposte qui. Ad esempio, potresti semplicemente scrivere:

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

I blocchi consentono di acquisire elenchi di parametri arbitrari, oggetti di riferimento e variabili.

Implementazione di supporto (di base):

@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

Esempio:

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

Vedi anche la risposta di Michael (+1) per un altro esempio.


3

Consiglio sempre di utilizzare NSMutableArray come oggetto da trasmettere. Questo perché puoi quindi passare diversi oggetti, come il pulsante premuto e altri valori. NSNumber, NSInteger e NSString sono solo contenitori di un certo valore. Assicurati che quando ottieni l'oggetto dall'array a cui fai riferimento a un tipo di contenitore corretto. Devi trasferire i container NS. Lì puoi testare il valore. Ricorda che i contenitori usano isEqual quando i valori vengono confrontati.

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

2

Volevo anche farlo, ma con un metodo che riceve un parametro BOOL. Racchiudendo il valore bool con NSNumber, IL VALORE NON È RIUSCITO. Non ho idea del perché.

Quindi ho finito per fare un semplice hack. Metto il parametro richiesto in un'altra funzione fittizia e chiamo quella funzione utilizzando performSelector, dove withObject = nil;

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

-(void)dummyCaller {

[self myFunction:YES];

}

Vedi il mio commento sopra sulla risposta dei danni. Sembra che, poiché il -performSelector[...]metodo si aspetta un oggetto (invece di un tipo di dati primitivo) e non sa che il selettore chiamato accetta un booleano, forse l'indirizzo (valore del puntatore) NSNumberdell'istanza è 'cast' ciecamente a BOOL(= diverso da zero, cioè TRUE). Forse il runtime potrebbe essere più intelligente di questo e riconoscere un booleano zero racchiuso in un NSNumber!
Nicolas Miari

Sono d'accordo. Immagino che una cosa migliore da fare sia evitare del tutto BOOL e utilizzare l'intero 0 per NO e 1 per YES e racchiuderlo con NSNumber o NSValue.
GeneCode

Questo cambia qualcosa? Se è così, improvvisamente sono davvero deluso da Cocoa :)
Nicolas Miari

2

Trovo che il modo più veloce (ma un po 'sporco) per farlo sia invocare direttamente objc_msgSend. Tuttavia, è pericoloso invocarlo direttamente perché è necessario leggere la documentazione e assicurarsi di utilizzare la variante corretta per il tipo di valore restituito e perché objc_msgSend è definito come vararg per comodità del compilatore ma in realtà è implementato come colla di assemblaggio veloce . Ecco un po 'di codice usato per chiamare un metodo delegato - [delegate integerDidChange:] che accetta un singolo argomento intero.

#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);
}

Questo prima salva il selettore poiché ci faremo riferimento più volte e sarebbe facile creare un errore di battitura. Quindi verifica che il delegato risponda effettivamente al selettore: potrebbe essere un protocollo opzionale. Quindi crea un tipo di puntatore a funzione che specifica la firma effettiva del selettore. Tieni presente che tutti i messaggi Objective-C hanno due primi argomenti nascosti, l'oggetto che viene messaggiato e il selettore che viene inviato. Quindi creiamo un puntatore a funzione del tipo appropriato e lo impostiamo in modo che punti alla funzione objc_msgSend sottostante. Tieni presente che se il valore restituito è un float o uno struct, devi utilizzare una variante diversa di objc_msgSend. Infine, invia il messaggio utilizzando lo stesso macchinario che Objective-C usa sotto i fogli.


"Tieni presente che se stai inviando float o struct ..." intendi restituire float o
struct

Queste tre righe ti danno l'indipendenza dai tipi e garantiscono che il numero di argomenti sia quello che il tuo selettore si aspetta. objc_msgSend è una funzione vararg che è effettivamente implementata come colla di assemblaggio, quindi se non scrivi typecast, puoi dargli qualsiasi cosa.
Jason Harris

1

Potresti semplicemente usare NSTimer per chiamare un selettore:

[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(yourMethod:) userInfo:nil repeats:NO]

Ti stai perdendo l'argomento. L'argomento yourMethod:nel tuo esempio è il timer stesso e non hai passato nulla per userInfo. Anche se lo userInfousassi, dovresti comunque incassare il valore, come hanno suggerito i danni e Remus Rusanu.
Peter Hosey

-1 per non utilizzare performSelector e creare istanze NSTimer non necessarie
Sviluppatore iOS di Applicasa

1

La chiamata a performSelector con un NSNumber o un altro NSValue non funzionerà. Invece di utilizzare il valore di NSValue / NSNumber, eseguirà effettivamente il cast del puntatore a un int, float o qualsiasi altra cosa e lo utilizzerà.

Ma la soluzione è semplice e ovvia. Crea NSInvocation e chiama

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


0

Forse ... ok, molto probabilmente, mi manca qualcosa, ma perché non creare semplicemente un tipo di oggetto, ad esempio NSNumber, come contenitore per la tua variabile di tipo non oggetto, come CGFloat?

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

[self performSelector:@selector(MyCalculatorMethod:) withObject:myNumber afterDelay:5.0];

La domanda riguarda il passaggio di tipi primitivi, non oggetti NSNumber.
Rudolf Adamkovič

La domanda presuppone che l'OP abbia solo accesso esterno al metodo e non possa modificare la firma.
Alex Grey
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.