Como usar performSelector: withObject: afterDelay: com tipos primitivos em Cocoa?

97

O NSObjectmétodo performSelector:withObject:afterDelay:me permite invocar um método no objeto com um argumento de objeto após um certo tempo. Ele não pode ser usado para métodos com um argumento não objeto (por exemplo, ints, floats, structs, ponteiros não objeto, etc.).

Qual é a maneira mais simples de obter a mesma coisa com um método com um argumento não objeto? Sei que para regular performSelector:withObject:, a solução é usar NSInvocation(o que por sinal é bem complicado). Mas não sei como lidar com a parte do "atraso".

Obrigado,

user102008
fonte
É um pouco hackeado, mas acho útil escrever código rápido. Algo como: id object = [array performSelector: @selector (objectAtIndex :) withObject: (__bridge_transfer) (void *) 1]; . Se o argumento for 0, você nem mesmo precisa de uma conversão de ponte porque 0 é "especial" e id é compatível com ele.
Ramy Al Zuhouri

Respostas:

72

Aqui está o que eu costumava chamar de algo que não conseguia mudar 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];
Morty
fonte
Por que não apenas fazer [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? Ou você quer dizer que a invokemensagem está no método que você executa após o atraso?
Peter Hosey
Ah sim, esqueci de dizer .. `[anInvocation performSelector: @selector (invoke) afterDelay: 0.5];
Morty de
24
apenas uma nota lateral para quem não sabe, começamos a adicionar argumentos do índice 2 porque os índices 0 e 1 são reservados para argumentos ocultos "self" e "_cmd".
Vishal Singh
35

Apenas envolva o float, boolean, int ou similar em um NSNumber.

Para structs, não conheço uma solução prática, mas você poderia fazer uma classe ObjC separada que possui tal struct.

prejudica
fonte
5
Crie um método wrapper, -delayedMethodWithArgument: (NSNumber *) arg. Faça com que ele chame o método original após extrair o primitivo do NSNumber.
James Williams
1
Entendido. Então, não tenho certeza de uma solução elegante, mas você poderia adicionar outro método que se destina a ser o alvo de seu performSelector :, que descompacta o NSNumber e executa o seletor final no método que você tem em mente. Outra ideia que você pode explorar é criar uma categoria em NSObject que adiciona perforSelector: withInt: ... (e semelhantes).
prejudica
32
NSValue (a superclasse de NSNumber) envolverá tudo o que você fornecer. Se você quiser envolver uma instância de 'struct foo' chamada 'bar', você faria '[NSValue valueWithBytes: & bar objCType: @encode (struct foo)];'
Jim Dovey
2
Você pode usar NSValue para envolver uma estrutura. De qualquer forma, NSNumber / NSValue é a solução correta.
Peter Hosey
6
Confirmado: DEVE passar nilpor um BOOLparâmetro para receber NO( FALSE)
Nicolas Miari 26/06/12
13

NÃO USE ESTA RESPOSTA. EU SÓ DEIXOU PARA FINS HISTÓRICOS. VEJA OS COMENTÁRIOS ABAIXO.

Existe um truque simples se for um parâmetro BOOL.

Passe nulo para NÃO e auto para SIM. nil é convertido para o valor BOOL de NO. self é convertido para o valor BOOL de YES.

Esta abordagem falha se for qualquer coisa diferente de um parâmetro BOOL.

Assumir a si mesmo é um 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];

fonte
Eu acredito que os valores BOOL devem ser sempre 0 ou 1. É verdade que a maioria do código não se importará e simplesmente fará um if ()teste nele, mas é concebível que algum código dependerá de ele ser 0 ou 1, e assim ganhou não trate algum valor de endereço grande como sendo igual a 1 (SIM).
user102008
1
Obrigado por isso, não queria criar um wrapper ou escrever uma sintaxe GCD feia para fazer isso.
0xSina
10
NÃO USE NINGUÉM . Essa abordagem é a fonte de bugs sérios, difíceis de encontrar. Imagine selfcom endereço como 0x0123400. Com essa abordagem, você receberá NÃO em vez de SIM em 0,4% dos casos. Pior, com essa probabilidade, essa solução pode passar em todos os testes e revelar depois.
Oleg Trakhman
1
UPD: seria obtido NÃO em vez de SIM com 6% de probabilidade por causa do alinhamento da memória.
Oleg Trakhman
4
@iVishal Confira as curvas
fechadas de
8

Talvez NSValue , apenas certifique-se de que seus ponteiros ainda sejam válidos após o atraso (ou seja, nenhum objeto alocado na pilha).

Remus Rusanu
fonte
Algum método antigo irá "desempacotar" e NSValue(se possível) para o esperado type?
Alex Gray
8

Eu sei que esta é uma pergunta antiga, mas se você estiver criando iOS SDK 4+, você pode usar blocos para fazer isso com muito pouco esforço e torná-lo mais legível:

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
fonte
Isso cria um loop de retenção. Para que isso funcione corretamente, você precisa fazer uma referência fraca a si mesmo e usar essa referência para executar o seletor. "__block typeof (self) weakSelf = self;"
Tim Wachter
1
Você não precisa de uma referência fraca aqui porque self não retém o bloco (não há loop de retenção). Provavelmente, é preferível usar uma referência forte, pois self pode ser desalocado de outra forma. Consulte: stackoverflow.com/a/19018633/251687
Sebastien Martin
6

PerformSelector: WithObject sempre pega um objeto, então para passar argumentos como int / double / float etc ..... Você pode usar algo assim.

//NSNumber is an object..

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

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

     [txtUsername setAlpha: [number floatValue] ];
    }

Da mesma forma, você pode usar [NSNumber numberWithInt:] etc .... e no método de recebimento, você pode converter o número em seu formato como [número int] ou [número duplo].

Agostinho
fonte
1
Se alguém estiver restrito a + performSelector: withObject: +, então esta é a única resposta certa postada, IMO. Mas a resposta de @MichaelGaylord usando blocos é mais limpa, se essa for uma opção para você.
big_m
5

Os blocos são o caminho a percorrer. Você pode ter parâmetros complexos, segurança de tipo, e é muito mais simples e seguro do que a maioria das respostas antigas aqui. Por exemplo, você pode simplesmente escrever:

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

Os blocos permitem que você capture listas de parâmetros arbitrários, objetos de referência e variáveis.

Implementação de apoio (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

Exemplo:

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

Veja também a resposta de Michael (+1) para outro exemplo.

justin
fonte
3

Eu sempre recomendo que você use NSMutableArray como o objeto a ser transmitido. Isso porque você pode passar vários objetos, como o botão pressionado e outros valores. NSNumber, NSInteger e NSString são apenas contêineres de algum valor. Certifique-se de que, ao obter o objeto da matriz, você se refere a um tipo de recipiente correto. Você precisa passar os contêineres NS. Lá você pode testar o valor. Lembre-se de que os contêineres usam isEqual quando os valores são comparados.

#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
fonte
2

Eu também queria fazer isso, mas com um método que recebe um parâmetro BOOL. Envolvendo o valor bool com NSNumber, FALHA AO PASSAR O VALOR. Eu não tenho ideia do porquê.

Então acabei fazendo um hack simples. Eu coloco o parâmetro necessário em outra função fictícia e chamo essa função usando o performSelector, onde withObject = nil;

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

-(void)dummyCaller {

[self myFunction:YES];

}
GeneCode
fonte
Veja meu comentário acima sobre a resposta aos danos. Parece que, uma vez que o -performSelector[...]método espera um objeto (em vez de um tipo de dados primitivo) e não sabe que o seletor chamado aceita um booleano, talvez o endereço (valor do ponteiro) da NSNumberinstância seja cegamente 'convertido' em BOOL(= diferente de zero, ou seja TRUE). Talvez o tempo de execução pudesse ser mais inteligente do que isso e reconhecer um booleano zero envolvido em um NSNumber!
Nicolas Miari
Concordo. Eu acho que a melhor coisa a fazer é evitar o BOOL completamente e usar o inteiro 0 para NÃO e 1 para SIM, e embrulhar isso com NSNumber ou NSValue.
GeneCode
Isso muda alguma coisa? Se sim, de repente estou realmente desapontado com Cocoa :)
Nicolas Miari
2

Acho que a maneira mais rápida (mas um tanto suja) de fazer isso é invocando objc_msgSend diretamente. No entanto, é perigoso invocá-lo diretamente porque você precisa ler a documentação e certificar-se de que está usando a variante correta para o tipo de valor de retorno e porque objc_msgSend é definido como vararg para conveniência do compilador, mas na verdade é implementado como cola de montagem rápida . Aqui está um código usado para chamar um método delegado - [delegate integerDidChange:] que leva um único argumento inteiro.

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

Isso primeiro salva o seletor, pois vamos nos referir a ele várias vezes e seria fácil criar um erro de digitação. Em seguida, verifica se o delegado realmente responde ao seletor - pode ser um protocolo opcional. Em seguida, ele cria um tipo de ponteiro de função que especifica a assinatura real do seletor. Lembre-se de que todas as mensagens Objective-C têm dois primeiros argumentos ocultos, o objeto que está sendo enviado e o seletor que está sendo enviado. Em seguida, criamos um ponteiro de função do tipo apropriado e o configuramos para apontar para a função objc_msgSend subjacente. Lembre-se de que, se o valor de retorno for um float ou struct, você precisará usar uma variante diferente de objc_msgSend. Finalmente, envie a mensagem usando o mesmo mecanismo que Objective-C usa sob as folhas.

Jason Harris
fonte
"Lembre-se de que se você estiver enviando flutuadores ou structs ...", você quer dizer devolvendo flutuadores ou structs
user102008
Essas três linhas fornecem segurança de digitação e garantem que o número de argumentos é o que seu seletor espera. objc_msgSend é uma função vararg que na verdade é implementada como cola de montagem, portanto, se você não fizer um typecast, poderá fornecer qualquer coisa.
Jason Harris
1

Você poderia apenas usar NSTimer para chamar um seletor:

[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(yourMethod:) userInfo:nil repeats:NO]
Unis
fonte
Você está perdendo o argumento. O argumento para yourMethod:em seu exemplo é o próprio cronômetro, e você não passou por nada userInfo. Mesmo se você usasse userInfo, ainda teria que encaixotar o valor, como prejudica e Remus Rusanu sugeriu.
Peter Hosey
-1 por não usar performSelector e criar instância NSTimer desnecessária
Desenvolvedor iOS Applicasa
1

Chamar performSelector com um NSNumber ou outro NSValue não funcionará. Em vez de usar o valor de NSValue / NSNumber, ele efetivamente lançará o ponteiro para um int, float ou qualquer outra coisa e usará isso.

Mas a solução é simples e óbvia. Crie o NSInvocation e chame

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

Anomia
fonte
0

Talvez ... ok, muito provavelmente, estou faltando alguma coisa, mas por que não apenas criar um tipo de objeto, digamos NSNumber, como um contêiner para sua variável de tipo não-objeto, como CGFloat?

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

[self performSelector:@selector(MyCalculatorMethod:) withObject:myNumber afterDelay:5.0];
Jim Hillhouse
fonte
A questão é sobre passar tipos primitivos, não objetos NSNumber.
Rudolf Adamkovič
A pergunta supõe que o OP só tenha acesso externo ao método e não possa alterar a assinatura.
Alex Gray,