Jak używać performSelector: withObject: afterDelay: z typami prymitywnymi w kakao?

97

NSObjectMetoda performSelector:withObject:afterDelay:pozwala mi wywołać metodę na obiekt z argumentem obiektu po pewnym czasie. Nie można jej używać dla metod z argumentem niebędącym przedmiotem (np. Ints, floats, structs, non-object pointers itp.).

Jaki jest najprostszy sposób osiągnięcia tego samego za pomocą metody z argumentem niebędącym przedmiotem? Wiem, że na performSelector:withObject:co dzień rozwiązaniem jest użycie NSInvocation(co przy okazji jest naprawdę skomplikowane). Ale nie wiem, jak sobie poradzić z częścią „opóźnienia”.

Dzięki,

user102008
źródło
To trochę hackerskie, ale uważam, że przydatne jest pisanie szybkiego kodu. Coś jak: id object = [tablica performSelector: @selector (objectAtIndex :) withObject: (__bridge_transfer) (void *) 1]; . Jeśli argumentem jest 0, nie potrzebujesz nawet rzutowania mostu, ponieważ 0 jest „specjalne” i id jest z nim zgodne.
Ramy Al Zuhouri

Odpowiedzi:

72

Oto, co zwykłem nazywać czymś, czego nie mogłem zmienić za pomocą 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
źródło
Dlaczego po prostu nie zrobić [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? Czy masz na myśli, że invokewiadomość jest w metodzie, którą wykonujesz po opóźnieniu?
Peter Hosey
Ach tak, zapomniałem powiedzieć ... `[anInvocation performSelector: @selector (invoke) afterDelay: 0.5];
Morty
24
uwaga na marginesie dla tych, którzy nie wiedzą, zaczynamy dodawać argumenty z indeksu 2, ponieważ indeksy 0 i 1 są zarezerwowane dla ukrytych argumentów „self” i „_cmd”.
Vishal Singh
35

Po prostu zawiń float, boolean, int lub podobny w NSNumber.

W przypadku struktur nie znam wygodnego rozwiązania, ale możesz stworzyć osobną klasę ObjC, która będzie właścicielem takiej struktury.

szkody
źródło
5
Utwórz metodę opakowującą, -delayedMethodWithArgument: (NSNumber *) arg. Niech wywoła oryginalną metodę po wyciągnięciu prymitywu z NSNumber.
James Williams
1
Zrozumiany. W takim razie nie jestem pewien eleganckiego rozwiązania, ale możesz dodać inną metodę, która jest przeznaczona jako cel twojego performSelector :, która rozpakowuje NSNumber i wykonuje ostateczny selektor na metodzie, którą masz obecnie na myśli. Innym pomysłem, który możesz zbadać, jest utworzenie kategorii na NSObject, która dodaje perforSelector: withInt: ... (i podobne).
szkodzi
32
NSValue (nadklasa NSNumber) zawinie wszystko, co mu podasz. Jeśli chcesz opakować instancję „struct foo” o nazwie „bar”, zrób to „[NSValue valueWithBytes: & bar objCType: @encode (struct foo)];”
Jim Dovey
2
Możesz użyć NSValue do zawijania struktury. Tak czy inaczej, NSNumber / NSValue jest właściwym rozwiązaniem.
Peter Hosey
6
Potwierdzone: MUSI przejść nildo BOOLparametru, aby otrzymać NO( FALSE)
Nicolas Miari
13

NIE UŻYWAJ TEJ ODPOWIEDZI. POZOSTAWIŁEM JEDYNIE W CELACH HISTORYCZNYCH. ZOBACZ PONIŻSZE UWAGI.

Istnieje prosta sztuczka, jeśli jest to parametr typu BOOL.

Pomiń zero na NIE, a siebie na TAK. zero jest rzutowane na wartość BOOL NO. self jest rzutowane na wartość BOOL równą YES.

To podejście nie działa, jeśli jest czymś innym niż parametr BOOL.

Zakładając, że ja jest 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];

źródło
Uważam, że wartości BOOL powinny zawsze wynosić 0 lub 1. Prawdą jest, że większość kodu nie będzie się tym przejmować i po prostu wykona if ()na nim test, ale można sobie wyobrazić, że jakiś kod będzie zależał od tego, że będzie 0 lub 1, a zatem wygrywa nie traktuj dużej wartości adresu jako takiej samej jak 1 (TAK).
user102008
1
Dzięki za to, nie chciałem w tym celu tworzyć opakowania ani pisać brzydkiej składni GCD.
0xSina
10
NIE UŻYWAJ GO NIGDY . Takie podejście jest źródłem poważnych błędów, trudnych do znalezienia. Wyobraź sobie selftaki adres 0x0123400. Dzięki temu podejściu NIE otrzymasz TAK w 0,4% przypadków. Co gorsza, z takim prawdopodobieństwem to rozwiązanie może przejść wszystkie testy i ujawnić się później.
Oleg Trakhman
1
UPD: można by NIE zamiast TAK z 6% prawdopodobieństwem z powodu wyrównania pamięci.
Oleg Trakhman
4
@iVishal Zobacz ostre zakręty BOOL-a
Farray,
8

Być może NSValue , po prostu upewnij się, że wskaźniki są nadal aktualne po opóźnieniu (tj. Nie ma obiektów przydzielonych na stosie).

Remus Rusanu
źródło
Czy jakaś stara metoda „rozpakuje” NSValue(jeśli to możliwe) zgodnie z oczekiwaniami type?
Alex Gray,
8

Wiem, że to stare pytanie, ale jeśli tworzysz iOS SDK 4+, możesz użyć bloków, aby to zrobić przy niewielkim wysiłku i uczynić go bardziej czytelnym:

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
źródło
Tworzy to pętlę zatrzymania. Aby to działało poprawnie, musisz zrobić słabe odniesienie do siebie i użyć tego odniesienia do wykonania selektora. "__block typeof (self) słabySelf = siebie;"
Tim Wachter
1
Nie potrzebujesz tutaj słabego odniesienia, ponieważ self nie zachowuje bloku (nie ma pętli retain). Prawdopodobnie lepiej jest użyć silnego odniesienia, ponieważ w przeciwnym razie self może zostać zwolnione. Zobacz: stackoverflow.com/a/19018633/251687
Sebastien Martin
6

PerformSelector: WithObject zawsze pobiera obiekt, więc aby przekazać argumenty takie jak int / double / float itp ..... Możesz użyć czegoś takiego.

//NSNumber is an object..

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

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

     [txtUsername setAlpha: [number floatValue] ];
    }

W ten sam sposób możesz użyć [NSNumber numberWithInt:] etc .... iw metodzie odbioru możesz przekonwertować liczbę na swój format jako [number int] lub [number double].

Augustyna
źródło
1
Jeśli jeden jest ograniczony do + performSelector: withObject: +, to jest to jedyna poprawna opublikowana odpowiedź, IMO. Ale odpowiedź @ MichaelGaylord za pomocą bloków jest czystsza, jeśli to dla ciebie opcja.
big_m
5

Bloki są do zrobienia. Możesz mieć złożone parametry, bezpieczeństwo typów i jest to o wiele prostsze i bezpieczniejsze niż większość starych odpowiedzi tutaj. Na przykład możesz po prostu napisać:

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

Bloki umożliwiają przechwytywanie dowolnych list parametrów, obiektów referencyjnych i zmiennych.

Implementacja wspierająca (podstawowa):

@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

Przykład:

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

Zobacz także odpowiedź Michaela (+1) dla innego przykładu.

Justin
źródło
3

Zawsze zalecałbym używanie NSMutableArray jako obiektu do przekazania. Dzieje się tak, ponieważ możesz następnie przekazać kilka obiektów, takich jak naciśnięty przycisk i inne wartości. NSNumber, NSInteger i NSString to tylko kontenery o pewnej wartości. Upewnij się, że po uzyskaniu obiektu z tablicy, do której się odwołujesz, do odpowiedniego typu kontenera. Musisz przekazać kontenery NS. Tam możesz sprawdzić wartość. Pamiętaj, że kontenery używają isEqual podczas porównywania wartości.

#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
źródło
2

Ja też chciałem to zrobić, ale metodą, która otrzymuje parametr BOOL. Zawijanie wartości bool przy użyciu NSNumber NIE POWIODŁO SIĘ DO PRZESŁANIA WARTOŚCI. Nie mam pojęcia dlaczego.

Skończyło się na tym, że zrobiłem prosty hack. Umieściłem wymagany parametr w innej funkcji fikcyjnej i wywołałem tę funkcję za pomocą performSelector, gdzie withObject = nil;

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

-(void)dummyCaller {

[self myFunction:YES];

}
GeneCode
źródło
Zobacz mój komentarz powyżej dotyczący odpowiedzi krzywd. Wygląda na to, że ponieważ -performSelector[...]metoda oczekuje obiektu (zamiast pierwotnego typu danych) i nie wie, że wywoływany selektor akceptuje wartość logiczną, być może adres (wartość wskaźnika) NSNumberinstancji jest ślepo „rzutowany” na BOOL(= niezerowe, tj TRUE.). Być może środowisko wykonawcze mogłoby być sprytniejsze niż to i rozpoznać zerową wartość logiczną opakowaną w NSNumber!
Nicolas Miari
Zgadzam się. Wydaje mi się, że lepszym rozwiązaniem jest całkowite uniknięcie BOOL i użycie liczby całkowitej 0 dla NIE i 1 dla TAK, a następnie owinięcie tego NSNumber lub NSValue.
GeneCode
Czy to coś zmienia? Jeśli tak, nagle jestem naprawdę rozczarowany kakao :)
Nicolas Miari,
2

Uważam, że najszybszym (ale nieco brudnym) sposobem na to jest bezpośrednie wywołanie objc_msgSend. Jednak wywołanie go bezpośrednio jest niebezpieczne, ponieważ musisz przeczytać dokumentację i upewnić się, że używasz prawidłowego wariantu dla typu wartości zwracanej, a ponieważ objc_msgSend jest zdefiniowany jako vararg dla wygody kompilatora, ale w rzeczywistości jest zaimplementowany jako szybki klej montażowy . Oto kod używany do wywołania metody delegata - [delegate integerDidChange:], która przyjmuje pojedynczy argument będący liczbą całkowitą.

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

Najpierw zapisuje selektor, ponieważ będziemy odwoływać się do niego wiele razy i łatwo byłoby wprowadzić literówkę. Następnie sprawdza, czy delegat faktycznie odpowiada selektorowi - może to być protokół opcjonalny. Następnie tworzy typ wskaźnika funkcji, który określa rzeczywistą sygnaturę selektora. Należy pamiętać, że wszystkie komunikaty Objective-C mają dwa ukryte pierwsze argumenty, wysyłany obiekt i wysyłany selektor. Następnie tworzymy wskaźnik funkcji odpowiedniego typu i ustawiamy go tak, aby wskazywał na podstawową funkcję objc_msgSend. Pamiętaj, że jeśli zwracana wartość jest zmiennoprzecinkową lub strukturą, musisz użyć innego wariantu objc_msgSend. Na koniec wyślij wiadomość za pomocą tej samej maszyny, której używa Objective-C pod arkuszami.

Jason Harris
źródło
„Pamiętaj, że jeśli wysyłasz elementy zmiennoprzecinkowe lub struktury…” masz na myśli zwracanie
elementów zmiennoprzecinkowych
Te trzy wiersze zapewniają bezpieczeństwo typów i gwarantują, że liczba argumentów jest zgodna z oczekiwaniami selektora. objc_msgSend to funkcja vararg, która w rzeczywistości jest zaimplementowana jako klej montażowy, więc jeśli nie używasz typecast, możesz dać jej wszystko.
Jason Harris
1

Możesz po prostu użyć NSTimer do wywołania selektora:

[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(yourMethod:) userInfo:nil repeats:NO]
Unis
źródło
Brakuje ci argumentu. Argumentem do yourMethod:w twoim przykładzie jest sam licznik czasu, a nie przekazałeś niczego dla userInfo. Nawet gdybyś użył userInfo, nadal musiałbyś podnosić wartość, jak sugerował Harms i Remus Rusanu.
Peter Hosey,
-1 za nieużywanie performSelector i tworzenie niepotrzebnej instancji NSTimer
Applicasa iOS
1

Wywołanie performSelector z NSNumber lub innym NSValue nie zadziała. Zamiast używać wartości NSValue / NSNumber, efektywnie rzutuje wskaźnik na int, float lub cokolwiek innego i używa tego.

Ale rozwiązanie jest proste i oczywiste. Utwórz NSInvocation i wywołaj

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

Dezorganizacja
źródło
0

Pehaps ... ok, bardzo prawdopodobne, że czegoś mi brakuje, ale dlaczego nie utworzyć po prostu typu obiektu, powiedzmy NSNumber, jako kontenera dla zmiennej niebędącej typem obiektowym, takiej jak CGFloat?

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

[self performSelector:@selector(MyCalculatorMethod:) withObject:myNumber afterDelay:5.0];
Jim Hillhouse
źródło
Pytanie dotyczy przekazywania typów pierwotnych, a nie obiektów NSNumber.
Rudolf Adamkovič
Pytanie zakłada, że ​​PO ma tylko zewnętrzny dostęp do metody i nie może zmienić podpisu.
Alex Gray,