Comment utiliser performSelector: withObject: afterDelay: avec des types primitifs dans Cocoa?

97

La NSObjectméthode performSelector:withObject:afterDelay:me permet d'invoquer une méthode sur l'objet avec un argument d'objet après un certain temps. Il ne peut pas être utilisé pour les méthodes avec un argument non-objet (par exemple ints, floats, structs, pointeurs non-objet, etc.).

Quelle est la manière la plus simple de réaliser la même chose avec une méthode avec un argument non-objet? Je sais que pour les réguliers performSelector:withObject:, la solution est d'utiliser NSInvocation(ce qui d'ailleurs est vraiment compliqué). Mais je ne sais pas comment gérer la partie "delay".

Merci,

user102008
la source
C'est un peu hackish mais je trouve utile d'écrire du code rapide. Quelque chose comme: id object = [array performSelector: @selector (objectAtIndex :) withObject: (__bridge_transfer) (void *) 1]; . Si l'argument est 0, vous n'avez même pas besoin d'un bridge cast car 0 est "spécial" et id est compatible avec lui.
Ramy Al Zuhouri

Réponses:

72

Voici ce que j'appelais quelque chose que je ne pouvais pas changer en utilisant 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];
Morty
la source
Pourquoi ne pas le faire [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? Ou voulez-vous dire que le invokemessage est dans la méthode que vous exécutez après le délai?
Peter Hosey
Ah oui, j'ai oublié de dire .. `[une invocation performSelector: @selector (invoke) afterDelay: 0.5];
Morty
24
juste une note d'accompagnement pour ceux qui ne savent pas, nous commençons à ajouter des arguments à partir de l'index 2 car les index 0 et 1 sont réservés aux arguments cachés "self" et "_cmd".
Vishal Singh
35

Enveloppez simplement le float, boolean, int ou similaire dans un NSNumber.

Pour les structures, je ne connais pas de solution pratique, mais vous pouvez créer une classe ObjC distincte qui possède une telle structure.

mal
la source
5
Créez une méthode wrapper, -delayedMethodWithArgument: (NSNumber *) arg. Demandez-lui d'appeler la méthode d'origine après avoir extrait la primitive du NSNumber.
James Williams
1
Compris. Ensuite, je ne suis pas sûr d'une solution élégante, mais vous pouvez ajouter une autre méthode qui est destinée à être la cible de votre performSelector :, qui décompresse le NSNumber et effectue le sélecteur final sur la méthode que vous avez actuellement en tête. Une autre idée que vous pourriez explorer est de créer une catégorie sur NSObject qui ajoute perforSelector: withInt: ... (et similaire).
nuit
32
NSValue (la superclasse de NSNumber) encapsulera tout ce que vous lui donnez. Si vous voulez encapsuler une instance de 'struct foo' appelée 'bar', vous feriez '[NSValue valueWithBytes: & bar objCType: @encode (struct foo)];'
Jim Dovey
2
Vous pouvez utiliser NSValue pour encapsuler une structure. Dans tous les cas, NSNumber / NSValue est la bonne solution.
Peter Hosey
6
Confirmé: DOIT passer nilpour qu'un BOOLparamètre reçoive NO( FALSE)
Nicolas Miari
13

N'UTILISEZ PAS CETTE RÉPONSE. JE L'AI QUITTER UNIQUEMENT POUR DES FINS HISTORIQUES. VOIR LES COMMENTAIRES CI-DESSOUS.

Il y a une astuce simple s'il s'agit d'un paramètre BOOL.

Passez nul pour NON et auto pour OUI. nil est converti en valeur BOOL de NO. self est converti à la valeur BOOL de YES.

Cette approche se décompose s'il s'agit d'autre chose qu'un paramètre BOOL.

En supposant que le soi est un 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];

la source
Je crois que les valeurs BOOL devraient toujours être 0 ou 1. Il est vrai que la plupart du code ne s'en souciera pas et fera simplement un if ()test dessus, mais il est concevable que certains codes dépendent de 0 ou 1, et donc gagné Ne considérez pas une grande valeur d'adresse comme étant la même que 1 (OUI).
user102008
1
Merci pour cela, je ne voulais pas créer de wrapper ou écrire une vilaine syntaxe GCD pour le faire.
0xSina
10
NE L'UTILISEZ PAS PERSONNE . Cette approche est à l'origine de bugs sérieux, difficiles à trouver. Imaginez selfavec une adresse comme 0x0123400. Avec cette approche, vous obtiendrez NON insted de OUI dans 0,4% des cas. Pire, avec cette probabilité, cette solution peut passer tous les tests et se révèle plus tard.
Oleg Trakhman
1
UPD: On obtiendrait NON insted de OUI avec 6% de probabilité en raison de l'alignement de la mémoire.
Oleg Trakhman
4
@iVishal Découvrez les virages pointus de BOOL
Farray
8

Peut-être NSValue , assurez-vous simplement que vos pointeurs sont toujours valides après le délai (c'est-à-dire qu'aucun objet n'est alloué sur la pile).

Remus Rusanu
la source
Est-ce qu'une ancienne méthode "déballera" un NSValue(si possible) à l'attendu type?
Alex Gray
8

Je sais que c'est une vieille question, mais si vous créez le SDK iOS 4+, vous pouvez utiliser des blocs pour le faire avec très peu d'effort et le rendre plus lisible:

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
la source
Cela crée une boucle de rétention. Pour que cela fonctionne correctement, vous devez faire une faible référence à vous-même et utiliser cette référence pour effectuer le sélecteur. "__block typeof (self) lowSelf = self;"
Tim Wachter
1
Vous n'avez pas besoin d'une référence faible ici car self ne retient pas le bloc (il n'y a pas de boucle de rétention). Il est probablement préférable d'utiliser une référence forte, car le soi pourrait être désalloué autrement. Voir: stackoverflow.com/a/19018633/251687
Sebastien Martin
6

PerformSelector: WithObject prend toujours un objet, donc pour passer des arguments comme int / double / float etc ..... Vous pouvez utiliser quelque chose comme ça.

//NSNumber is an object..

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

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

     [txtUsername setAlpha: [number floatValue] ];
    }

De la même manière, vous pouvez utiliser [NSNumber numberWithInt:] etc .... et dans la méthode de réception, vous pouvez convertir le nombre dans votre format comme [nombre entier] ou [nombre double].

Augustine
la source
1
Si l'on est limité à + performSelector: withObject: +, alors c'est la seule bonne réponse publiée, IMO. Mais la réponse de @ MichaelGaylord à l'aide de blocs est plus propre, si c'est une option pour vous.
big_m
5

Les blocs sont la voie à suivre. Vous pouvez avoir des paramètres complexes, taper la sécurité, et c'est beaucoup plus simple et plus sûr que la plupart des anciennes réponses ici. Par exemple, vous pouvez simplement écrire:

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

Les blocs vous permettent de capturer des listes de paramètres arbitraires, des objets de référence et des variables.

Mise en œuvre de support (de 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

Exemple:

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

Voir également la réponse de Michael (+1) pour un autre exemple.

Justin
la source
3

Je recommanderais toujours que vous utilisiez NSMutableArray comme objet à transmettre. En effet, vous pouvez ensuite passer plusieurs objets, comme le bouton enfoncé et d'autres valeurs. NSNumber, NSInteger et NSString ne sont que des conteneurs d'une certaine valeur. Assurez-vous que lorsque vous obtenez l'objet du tableau auquel vous faites référence, un type de conteneur correct. Vous devez transmettre les conteneurs NS. Là, vous pouvez tester la valeur. N'oubliez pas que les conteneurs utilisent isEqual lorsque les valeurs sont comparées.

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

Je voulais aussi faire cela, mais avec une méthode qui reçoit un paramètre BOOL. Enveloppant la valeur booléenne avec NSNumber, ÉCHEC DE PASSER LA VALEUR. Je ne sais pas pourquoi.

J'ai donc fini par faire un simple hack. J'ai mis le paramètre requis dans une autre fonction factice et j'appelle cette fonction en utilisant performSelector, où withObject = nil;

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

-(void)dummyCaller {

[self myFunction:YES];

}
GeneCode
la source
Voir mon commentaire ci-dessus sur la réponse des torts. On dirait que, puisque la -performSelector[...]méthode attend un objet (au lieu d'un type de données primitif), et qu'elle ne sait pas que le sélecteur appelé accepte un booléen, peut-être que l'adresse (valeur du pointeur) de l' NSNumberinstance est aveuglément `` cast '' en BOOL(= non nul, c'est-à-dire TRUE). Peut-être que le runtime pourrait être plus intelligent que cela et reconnaître un zéro booléen enveloppé dans un NSNumber!
Nicolas Miari
Je suis d'accord. Je suppose qu'une meilleure chose à faire est d'éviter complètement BOOL et d'utiliser l'entier 0 pour NON et 1 pour OUI, et de l'envelopper avec NSNumber ou NSValue.
GeneCode
Cela change-t-il quelque chose? Si c'est le cas, du coup je suis vraiment déçu par Cocoa :)
Nicolas Miari
2

Je trouve que le moyen le plus rapide (mais un peu sale) de le faire est d'appeler directement objc_msgSend. Cependant, il est dangereux de l'invoquer directement car vous devez lire la documentation et vous assurer que vous utilisez la bonne variante pour le type de valeur de retour et parce que objc_msgSend est défini comme vararg pour la commodité du compilateur mais est en fait implémenté comme colle d'assemblage rapide . Voici du code utilisé pour appeler une méthode déléguée - [delegate integerDidChange:] qui prend un seul argument entier.

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

Cela enregistre d'abord le sélecteur car nous allons y faire référence plusieurs fois et il serait facile de créer une faute de frappe. Il vérifie ensuite que le délégué répond réellement au sélecteur - il peut s'agir d'un protocole facultatif. Il crée ensuite un type de pointeur de fonction qui spécifie la signature réelle du sélecteur. Gardez à l'esprit que tous les messages Objective-C ont deux premiers arguments cachés, l'objet étant envoyé et le sélecteur envoyé. Ensuite, nous créons un pointeur de fonction du type approprié et le définissons pour qu'il pointe vers la fonction objc_msgSend sous-jacente. Gardez à l'esprit que si la valeur de retour est un float ou un struct, vous devez utiliser une variante différente de objc_msgSend. Enfin, envoyez le message en utilisant le même mécanisme qu'Objective-C utilise sous les feuilles.

Jason Harris
la source
"Gardez à l'esprit que si vous envoyez des floats ou des structs ..." vous voulez dire renvoyer des floats ou des
structs
Ces trois lignes vous donnent la sécurité de type et garantissent que le nombre d'arguments est ce que votre sélecteur attend. objc_msgSend est une fonction vararg qui est en fait implémentée en tant que colle d'assemblage, donc si vous ne tapez pas, vous pouvez lui donner n'importe quoi.
Jason Harris
1

Vous pouvez simplement utiliser NSTimer pour appeler un sélecteur:

[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(yourMethod:) userInfo:nil repeats:NO]
Unis
la source
Vous manquez l'argument. L'argument de yourMethod:dans votre exemple est le minuteur lui-même, et vous n'avez rien transmis pour userInfo. Même si vous aviez utilisé userInfo, vous auriez toujours à réduire la valeur, comme le suggéra Remus Rusanu.
Peter Hosey
-1 pour ne pas utiliser performSelector et créer une instance
NSTimer inutile
1

L'appel de performSelector avec un NSNumber ou une autre NSValue ne fonctionnera pas. Au lieu d'utiliser la valeur de NSValue / NSNumber, il convertira efficacement le pointeur en un int, float ou autre et l'utilisera.

Mais la solution est simple et évidente. Créez le NSInvocation et appelez

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

Anomie
la source
0

Peut-être ... ok, très probablement, il me manque quelque chose, mais pourquoi ne pas simplement créer un type d'objet, disons NSNumber, en tant que conteneur de votre variable de type non-objet, telle que CGFloat?

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

[self performSelector:@selector(MyCalculatorMethod:) withObject:myNumber afterDelay:5.0];
Jim Hillhouse
la source
La question est de passer des types primitifs, pas des objets NSNumber.
Rudolf Adamkovič
La question suppose que l'OP n'a qu'un accès externe à la méthode et ne peut pas modifier la signature.
Alex Gray