¿Cómo usar performSelector: withObject: afterDelay: con tipos primitivos en Cocoa?


97

El NSObjectmétodo performSelector:withObject:afterDelay:me permite invocar un método en el objeto con un argumento de objeto después de un cierto tiempo. No se puede utilizar para métodos con un argumento que no sea de objeto (por ejemplo, ints, floats, structs, punteros que no sean de objeto, etc.).

¿Cuál es la forma más sencilla de lograr lo mismo con un método con un argumento sin objeto? Sé que de forma regular performSelector:withObject:, la solución es usar NSInvocation(que por cierto es realmente complicado). Pero no sé cómo manejar la parte del "retraso".

Gracias,


Es un poco hack, pero me resulta útil escribir código rápido. Algo como: id object = [array performSelector: @selector (objectAtIndex :) withObject: (__bridge_transfer) (void *) 1]; . Si el argumento es 0, ni siquiera necesita una conversión puente porque 0 es "especial" y la identificación es compatible con él.
Ramy Al Zuhouri

Respuestas:


72

Esto es lo que solía llamar algo que no podía cambiar 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];

¿Por qué no hacerlo [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? ¿O quiere decir que el invokemensaje está en el método que realiza después de la demora?
Peter Hosey

Ah sí, me olvidé de decir .. `[anInvocation performSelector: @selector (invoke) afterDelay: 0.5];
Morty

24
sólo una nota al margen para aquellos que no saben, comenzamos a agregar argumentos del índice 2 porque los índices 0 y 1 están reservados para los argumentos ocultos "self" y "_cmd".
Vishal Singh

35

Simplemente envuelva el float, boolean, int o similar en un NSNumber.

Para las estructuras, no conozco una solución útil, pero podría crear una clase ObjC separada que posea dicha estructura.


5
Cree un método contenedor, -delayedMethodWithArgument: (NSNumber *) arg. Haga que llame al método original después de extraer la primitiva del NSNumber.
James Williams

1
Entendido. Entonces no estoy seguro de una solución elegante, pero podría agregar otro método que está destinado a ser el objetivo de su performSelector :, que descomprime el NSNumber y realiza el selector final en el método que tiene actualmente en mente. Otra idea que podría explorar es crear una categoría en NSObject que agregue perforSelector: withInt: ... (y similar).
daña

32
NSValue (la superclase de NSNumber) ajustará todo lo que le des. Si desea ajustar una instancia de 'struct foo' llamada 'bar', debería hacer '[NSValue valueWithBytes: & bar objCType: @encode (struct foo)];'
Jim Dovey

2
Puede usar NSValue para envolver una estructura. De cualquier manera, NSNumber / NSValue es la solución correcta.
Peter Hosey

6
Confirmado: DEBE pasar nilpara BOOLrecibir un parámetro NO( FALSE)
Nicolas Miari

13

NO USE ESTA RESPUESTA. SOLO LO HE DEJADO PARA FINES HISTÓRICOS. VER LOS COMENTARIOS A CONTINUACIÓN.

Hay un truco simple si es un parámetro BOOL.

Pase nulo por NO y self por SÍ. nil se convierte en el valor BOOL de NO. self se convierte en el valor BOOL de YES.

Este enfoque se rompe si no es un parámetro BOOL.

Asumir que uno mismo es una 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];

Creo que los valores BOOL siempre deben ser 0 o 1. Es cierto que a la mayoría de los códigos no les importará y simplemente harán una if ()prueba, pero es concebible que algún código dependa de que sea 0 o 1, y así ganó No trate un valor de dirección grande como si fuera igual a 1 (YES).
user102008

1
Gracias por eso, no quería crear un contenedor o escribir una sintaxis GCD desagradable para hacer eso.
0xSina

10
NO LO USE NADIE . Este enfoque es la fuente de errores graves, difíciles de encontrar. Imagínese selfcon una dirección como 0x0123400. Con este enfoque, obtendrá NO en respuesta a SÍ en el 0.4% de los casos. Peor, con esta probabilidad, esta solución puede pasar todas las pruebas y revelar más tarde.
Oleg Trakhman

1
UPD: Uno obtendría NO en lugar de SÍ con un 6% de probabilidad debido a la alineación de la memoria.
Oleg Trakhman

4
@iVishal Echa un vistazo a las esquinas afiladas de BOOL
Farray

8

Quizás NSValue , solo asegúrese de que sus punteros sigan siendo válidos después del retraso (es decir, no hay objetos asignados en la pila).


¿Algún método antiguo "desempaquetará" y NSValue(si es posible) lo esperado type?
Alex Gray

8

Sé que esta es una pregunta antigua, pero si está creando iOS SDK 4+, puede usar bloques para hacer esto con muy poco esfuerzo y hacerlo más legible:

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

Esto crea un bucle de retención. Para que esto funcione correctamente, debe hacer una referencia débil a sí mismo y usar esa referencia para realizar el selector. "__block typeof (self) debilSelf = self;"
Tim Wachter

1
No necesita una referencia débil aquí porque self no retiene el bloque (no hay un ciclo de retención). Probablemente sea preferible utilizar una referencia fuerte en realidad, ya que, de lo contrario, self podría desasignarse. Ver: stackoverflow.com/a/19018633/251687
Sebastien Martin

6

PerformSelector: WithObject siempre toma un objeto, así que para pasar argumentos como int / double / float, etc ..... Puedes usar algo como esto.

//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 misma manera puede usar [NSNumber numberWithInt:] etc .... y en el método de recepción puede convertir el número a su formato como [number int] o [number double].


1
Si uno está restringido a + performSelector: withObject: +, entonces esta es la única respuesta correcta publicada, IMO. Pero la respuesta de @ MichaelGaylord usando bloques es más limpia, si esa es una opción para usted.
big_m

5

Los bloques son el camino a seguir. Puede tener parámetros complejos, seguridad de tipos y es mucho más simple y seguro que la mayoría de las respuestas anteriores aquí. Por ejemplo, podría simplemente escribir:

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

Los bloques le permiten capturar listas de parámetros arbitrarias, objetos de referencia y variables.

Implementación de respaldo (básico):

@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

Ejemplo:

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

También vea la respuesta de Michael (+1) para ver otro ejemplo.


3

Siempre recomendaría que use NSMutableArray como el objeto para transmitir. Esto se debe a que luego puede pasar varios objetos, como el botón presionado y otros valores. NSNumber, NSInteger y NSString son solo contenedores de algún valor. Asegúrese de que cuando obtenga el objeto de la matriz al que se refiere a un tipo de contenedor correcto. Necesita pasar los contenedores NS. Allí puede probar el valor. Recuerde que los contenedores usan isEqual cuando se comparan valores.

#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

También quería hacer esto, pero con un método que recibe un parámetro BOOL. Envolviendo el valor bool con NSNumber, NO SE PODÍA PASAR EL VALOR. No tengo ni idea de porqué.

Así que terminé haciendo un truco simple. Pongo el parámetro requerido en otra función ficticia y llamo a esa función usando performSelector, donde withObject = nil;

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

-(void)dummyCaller {

[self myFunction:YES];

}

Vea mi comentario anterior sobre la respuesta de daños. Parece que, dado que el -performSelector[...]método espera un objeto (en lugar de un tipo de datos primitivo), y no sabe que el selector llamado acepta un booleano, tal vez la dirección (valor del puntero) de la NSNumberinstancia se 'envía' ciegamente a BOOL(= distinto de cero, es decir TRUE). Quizás el tiempo de ejecución podría ser más inteligente que esto y reconocer un booleano cero envuelto en un NSNumber!
Nicolas Miari

Estoy de acuerdo. Supongo que una mejor cosa es evitar BOOL por completo y usar el entero 0 para NO y 1 para YES, y envolverlo con NSNumber o NSValue.
GeneCode

¿Eso cambia algo? Si es así, de repente estoy realmente decepcionado con Cocoa :)
Nicolas Miari

2

Encuentro que la forma más rápida (pero algo sucia) de hacer esto es invocando objc_msgSend directamente. Sin embargo, es peligroso invocarlo directamente porque necesita leer la documentación y asegurarse de que está utilizando la variante correcta para el tipo de valor devuelto y porque objc_msgSend se define como vararg para la conveniencia del compilador, pero en realidad se implementa como pegamento de ensamblaje rápido . Aquí hay un código usado para llamar a un método delegado - [delegate integerDidChange:] que toma un único argumento entero.

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

Esto primero guarda el selector ya que vamos a hacer referencia a él varias veces y sería fácil crear un error tipográfico. Luego verifica que el delegado realmente responde al selector; podría ser un protocolo opcional. Luego crea un tipo de puntero de función que especifica la firma real del selector. Tenga en cuenta que todos los mensajes de Objective-C tienen dos primeros argumentos ocultos, el objeto al que se envía el mensaje y el selector que se envía. Luego creamos un puntero de función del tipo apropiado y lo configuramos para que apunte a la función objc_msgSend subyacente. Tenga en cuenta que si el valor de retorno es un flotante o una estructura, debe usar una variante diferente de objc_msgSend. Finalmente, envíe el mensaje utilizando la misma maquinaria que utiliza Objective-C debajo de las hojas.


"Tenga en cuenta que si envía flotantes o estructuras ..." se
refiere a

Esas tres líneas le brindan seguridad de tipos y garantizan que la cantidad de argumentos es la que espera su selector. objc_msgSend es una función vararg que en realidad se implementa como pegamento de ensamblaje, por lo que si no encasilla, puede darle cualquier cosa.
Jason Harris

1

Podría usar NSTimer para llamar a un selector:

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

Te estás perdiendo el argumento. El argumento de yourMethod:su ejemplo es el temporizador en sí, y no ha pasado nada userInfo. Incluso si lo usara userInfo, aún tendría que encajar el valor, como sugirieron Daños y Remus Rusanu.
Peter Hosey

-1 por no usar performSelector y crear una instancia NSTimer innecesaria
desarrollador de Applicasa iOS

1

Llamar a performSelector con un NSNumber u otro NSValue no funcionará. En lugar de usar el valor de NSValue / NSNumber, lanzará efectivamente el puntero a un int, float o lo que sea y lo usará.

Pero la solución es simple y obvia. Cree la NSInvocation y llame

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


0

Quizás ... ok, muy probablemente, me falta algo, pero ¿por qué no crear un tipo de objeto, digamos NSNumber, como un contenedor para su variable de tipo que no es de objeto, como CGFloat?

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

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

La pregunta es sobre pasar tipos primitivos, no objetos NSNumber.
Rudolf Adamkovič

La pregunta supone que el OP solo tiene acceso externo al método y no puede cambiar la firma.
Alex Gray
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.