Bagaimana cara menggunakan performSelector: withObject: afterDelay: dengan tipe primitif di Cocoa?

97

The NSObjectMetode performSelector:withObject:afterDelay:memungkinkan saya untuk memanggil metode pada objek dengan argumen objek setelah waktu tertentu. Ini tidak dapat digunakan untuk metode dengan argumen non-objek (misalnya ints, floats, struct, non-object pointers, dll.).

Apa cara paling sederhana untuk mencapai hal yang sama dengan metode dengan argumen non-objek? Saya tahu bahwa untuk reguler performSelector:withObject:, solusinya adalah dengan menggunakan NSInvocation(yang sebenarnya sangat rumit). Tapi saya tidak tahu bagaimana menangani bagian "penundaan".

Terima kasih,

pengguna102008
sumber
Agak hackish tapi menurut saya berguna untuk menulis kode cepat. Sesuatu seperti: id object = [array performSelector: @selector (objectAtIndex :) withObject: (__bridge_transfer) (void *) 1]; . Jika argumennya 0 Anda bahkan tidak perlu cast bridge karena 0 adalah "spesial" dan id kompatibel dengannya.
Ramy Al Zuhouri

Jawaban:

72

Inilah yang biasa saya sebut sesuatu yang tidak dapat saya ubah menggunakan 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
sumber
Kenapa tidak lakukan saja [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? Atau maksud Anda invokepesan tersebut ada dalam metode yang Anda lakukan setelah penundaan?
Peter Hosey
Ah ya, saya lupa bilang .. `[anInvocation performSelector: @selector (invoke) afterDelay: 0.5];
Morty
24
hanya catatan tambahan bagi mereka yang tidak tahu, kami mulai menambahkan argumen dari indeks 2 karena indeks 0 dan 1 disediakan untuk argumen tersembunyi "self" dan "_cmd".
Vishal Singh
35

Cukup bungkus float, boolean, int atau yang serupa di NSNumber.

Untuk struct, saya tidak tahu solusi yang berguna, tetapi Anda dapat membuat kelas ObjC terpisah yang memiliki struct tersebut.

bahaya
sumber
5
Buat metode pembungkus, -delayedMethodWithArgument: (NSNumber *) arg. Minta itu memanggil metode asli setelah menarik primitif keluar dari NSNumber.
James Williams
1
Dimengerti. Maka saya tidak yakin solusi yang elegan, tetapi Anda dapat menambahkan metode lain yang dimaksudkan sebagai target performSelector Anda :, yang membongkar NSNumber dan melakukan pemilih terakhir pada metode yang saat ini Anda pikirkan. Ide lain yang bisa Anda jelajahi adalah membuat kategori di NSObject yang menambahkan perforSelector: withInt: ... (dan sejenisnya).
merugikan
32
NSValue (superclass dari NSNumber) akan membungkus apa pun yang Anda berikan. Jika Anda ingin membungkus sebuah instance dari 'struct foo' yang disebut 'bar', Anda akan melakukan '[NSValue valueWithBytes: & bar objCType: @encode (struct foo)];'
Jim Dovey
2
Anda dapat menggunakan NSValue untuk membungkus struct. Bagaimanapun, NSNumber / NSValue adalah solusi yang tepat.
Peter Hosey
6
Dikonfirmasi: HARUS lulus niluntuk BOOLparameter untuk menerima NO( FALSE)
Nicolas Miari
13

JANGAN GUNAKAN JAWABAN INI. SAYA HANYA MENINGGALKANNYA UNTUK KEPERLUAN SEJARAH. LIHAT KOMENTAR DI BAWAH.

Ada trik sederhana jika itu adalah parameter BOOL.

Berikan nol untuk TIDAK dan diri sendiri untuk YA. nil dilemparkan ke nilai BOOL dari NO. self dilemparkan ke nilai BOOL dari YES.

Pendekatan ini rusak jika itu adalah selain parameter BOOL.

Mengasumsikan diri adalah seorang 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];

sumber
Saya percaya bahwa nilai BOOL harus selalu 0 atau 1. Memang benar bahwa sebagian besar kode tidak akan peduli dan hanya akan melakukan if ()tes padanya, tetapi dapat dibayangkan bahwa beberapa kode akan bergantung padanya menjadi 0 atau 1, dan dengan demikian menang perlakukan beberapa nilai alamat yang besar sama dengan 1 (YA).
pengguna102008
1
Terima kasih untuk itu, tidak ingin membuat pembungkus atau menulis sintaks GCD yang jelek untuk melakukan itu.
0xSina
10
JANGAN GUNAKAN SIAPA PUN . Pendekatan ini adalah sumber bug yang serius, sulit ditemukan. Bayangkan selfdengan alamat seperti 0x0123400. Dengan pendekatan ini, Anda TIDAK akan mendapatkan insting YA dalam 0,4% kasus. Lebih buruk lagi, dengan probabilitas ini, solusi ini dapat lulus semua tes dan muncul nanti.
Oleg Trakhman
1
UPD: Seseorang akan mendapatkan NO insted dari YES dengan kemungkinan 6% karena penyelarasan memori.
Oleg Trakhman
4
@iVishal Lihat sudut tajam BOOL
Farray
8

Mungkin NSValue , cukup pastikan pointer Anda masih valid setelah penundaan (mis. Tidak ada objek yang dialokasikan di stack).

Remus Rusanu
sumber
Akankah ada metode lama yang "membuka kotak" NSValue(jika mungkin) seperti yang diharapkan type?
Alex Grey
8

Saya tahu ini adalah pertanyaan lama tetapi jika Anda membuat iOS SDK 4+, Anda dapat menggunakan blok untuk melakukan ini dengan sedikit usaha dan membuatnya lebih mudah dibaca:

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
sumber
Ini menciptakan loop penahan. Agar ini berfungsi dengan baik, Anda perlu membuat referensi lemah ke diri sendiri dan menggunakan referensi itu untuk menjalankan selektor. "__block typeof (self) weakSelf = self;"
Tim Wachter
1
Anda tidak memerlukan referensi yang lemah di sini karena self tidak menahan blok (tidak ada loop penahan). Mungkin lebih baik menggunakan referensi yang kuat sebenarnya karena self mungkin akan dibatalkan alokasinya jika tidak. Lihat: stackoverflow.com/a/19018633/251687
Sebastien Martin
6

PerformSelector: WithObject selalu mengambil objek, jadi untuk meneruskan argumen seperti int / double / float dll ..... Anda dapat menggunakan sesuatu seperti ini.

//NSNumber is an object..

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

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

     [txtUsername setAlpha: [number floatValue] ];
    }

Dengan cara yang sama Anda dapat menggunakan [NSNumber numberWithInt:] dll .... dan dalam metode penerimaan Anda dapat mengonversi nomor ke dalam format Anda sebagai [number int] atau [number double].

Agustinus
sumber
1
Jika seseorang dibatasi untuk + performSelector: withObject: +, maka ini adalah satu-satunya jawaban yang benar diposting, IMO. Tetapi jawaban @ MichaelGaylord menggunakan blok lebih bersih, jika itu pilihan untuk Anda.
big_m
5

Blok adalah jalan yang harus ditempuh. Anda dapat memiliki parameter kompleks, jenis keamanan, dan itu jauh lebih sederhana dan lebih aman daripada kebanyakan jawaban lama di sini. Misalnya, Anda bisa menulis:

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

Pemblokiran memungkinkan Anda untuk menangkap daftar parameter arbitrer, objek referensi, dan variabel.

Implementasi Backing (dasar):

@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

Contoh:

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

Lihat juga jawaban Michael (+1) untuk contoh lainnya.

justin
sumber
3

Saya akan selalu merekomendasikan agar Anda menggunakan NSMutableArray sebagai objek untuk diteruskan. Ini karena Anda kemudian dapat melewatkan beberapa objek, seperti tombol yang ditekan dan nilai lainnya. NSNumber, NSInteger dan NSString hanyalah wadah dari beberapa nilai. Pastikan bahwa ketika Anda mendapatkan objek dari larik yang Anda rujuk ke tipe penampung yang benar. Anda harus meneruskan kontainer NS. Di sana Anda dapat menguji nilainya. Ingat bahwa kontainer menggunakan isEqual saat nilainya dibandingkan.

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

Saya juga ingin melakukan ini, tetapi dengan metode yang menerima parameter BOOL. Membungkus nilai bool dengan NSNumber, GAGAL MELULAI NILAI. Saya tidak tahu kenapa.

Jadi saya akhirnya melakukan peretasan sederhana. Saya meletakkan parameter yang diperlukan dalam fungsi dummy lain dan memanggil fungsi itu menggunakan performSelector, di mana withObject = nil;

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

-(void)dummyCaller {

[self myFunction:YES];

}
GeneCode
sumber
Lihat komentar saya di atas tentang jawaban bahaya. Sepertinya, karena -performSelector[...]metode mengharapkan objek (bukan tipe data primitif), dan tidak tahu bahwa selektor yang dipanggil menerima boolean, mungkin alamat (nilai penunjuk) dari NSNumberinstance secara membabi buta 'dilemparkan' ke BOOL(= bukan nol, yaitu TRUE). Mungkin runtime bisa lebih pintar dari ini dan mengenali nol boolean yang dibungkus dengan NSNumber!
Nicolas Miari
Saya setuju. Saya kira hal yang lebih baik untuk dilakukan adalah menghindari BOOL sama sekali dan menggunakan integer 0 untuk NO dan 1 untuk YES, dan membungkusnya dengan NSNumber atau NSValue.
GeneCode
Apakah itu mengubah sesuatu? Jika demikian, tiba-tiba saya benar-benar kecewa dengan Kakao :)
Nicolas Miari
2

Saya menemukan bahwa cara tercepat (tapi agak kotor) untuk melakukan ini adalah dengan memanggil objc_msgSend secara langsung. Namun, berbahaya untuk memanggilnya secara langsung karena Anda perlu membaca dokumentasinya dan memastikan bahwa Anda menggunakan varian yang benar untuk tipe nilai yang dikembalikan dan karena objc_msgSend didefinisikan sebagai vararg untuk kenyamanan kompilator tetapi sebenarnya diimplementasikan sebagai perekat perakitan cepat . Berikut beberapa kode yang digunakan untuk memanggil metode delegasi - [delegate integerDidChange:] yang menggunakan argumen integer tunggal.

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

Ini pertama kali menyimpan selektor karena kita akan merujuknya beberapa kali dan akan mudah membuat kesalahan ketik. Kemudian memverifikasi bahwa delegasi benar-benar menanggapi pemilih - ini mungkin protokol opsional. Ini kemudian membuat jenis penunjuk fungsi yang menentukan tanda tangan sebenarnya dari pemilih. Perlu diingat bahwa semua pesan Objective-C memiliki dua argumen pertama yang tersembunyi, objek yang sedang dikirimi pesan dan selektor yang sedang dikirim. Kemudian kita membuat penunjuk fungsi dari tipe yang sesuai dan mengaturnya untuk menunjuk ke fungsi objc_msgSend yang mendasarinya. Perlu diingat bahwa jika nilai yang dikembalikan adalah float atau struct, Anda perlu menggunakan varian objc_msgSend yang berbeda. Terakhir, kirim pesan menggunakan mesin yang sama dengan yang digunakan Objective-C di bawah lembaran.

Jason Harris
sumber
"Perlu diingat bahwa jika Anda mengirim float atau struct ..." yang Anda maksud adalah mengembalikan float atau struct
user102008
Ketiga baris tersebut memberi Anda keamanan tipe dan menjamin bahwa jumlah argumen sesuai dengan harapan pemilih Anda. objc_msgSend adalah fungsi vararg yang sebenarnya diimplementasikan sebagai lem perakitan jadi jika Anda tidak mengetik, Anda dapat memberikannya apa pun.
Jason Harris
1

Anda bisa menggunakan NSTimer untuk memanggil pemilih:

[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(yourMethod:) userInfo:nil repeats:NO]
Unis
sumber
Anda melewatkan argumennya. Argumen yourMethod:dalam contoh Anda adalah pengatur waktu itu sendiri, dan Anda belum mengirimkan apa pun userInfo. Bahkan jika kamu benar-benar menggunakan userInfo, kamu masih harus mengemas nilainya, seperti yang disarankan oleh bahaya dan Remus Rusanu.
Peter Hosey
-1 karena tidak menggunakan performSelector dan membuat instance NSTimer yang tidak diperlukan
Pengembang iOS Applicasa
1

Memanggil performSelector dengan NSNumber atau NSValue lain tidak akan berfungsi. Alih-alih menggunakan nilai NSValue / NSNumber, ini akan secara efektif mentransmisikan pointer ke int, float, atau apa pun dan menggunakannya.

Tetapi solusinya sederhana dan jelas. Buat NSInvocation dan panggil

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

Anomie
sumber
0

Mungkin ... ok, sangat mungkin, saya melewatkan sesuatu, tetapi mengapa tidak membuat tipe objek saja, katakanlah NSNumber, sebagai wadah untuk variabel tipe non-objek Anda, seperti CGFloat?

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

[self performSelector:@selector(MyCalculatorMethod:) withObject:myNumber afterDelay:5.0];
Jim Hillhouse
sumber
Pertanyaannya adalah tentang meneruskan tipe primitif, bukan objek NSNumber.
Rudolf Adamkovič
Pertanyaannya mengandaikan bahwa OP hanya memiliki akses eksternal ke metode dan tidak dapat mengubah tanda tangan.
Alex Grey