如何在 Cocoa 中对基本类型使用 PerformSelector: withObject: After耽搁: ?

NSObject方法 performSelector:withObject:afterDelay:允许我在一定时间后使用对象参数调用对象上的方法。它不能用于具有非对象参数的方法(例如 int、 float、 structs、非对象指针等)。

使用非对象参数的方法实现同样目的的 最简单的方法是什么?我知道,对于常规的 performSelector:withObject:,解决方案是使用 NSInvocation(顺便说一下,这真的很复杂)。但我不知道怎么处理“延迟”这部分。

谢谢,

144166 次浏览

Just wrap the float, boolean, int or similar in an NSNumber.

For structs, I don't know of a handy solution, but you could make a separate ObjC class that owns such a struct.

Perhaps NSValue, just make sure your pointers are still valid after the delay (ie. no objects allocated on stack).

Here is what I used to call something I couldn't change using 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];

You Could just use NSTimer to call a selector:

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

Pehaps...ok, very likely, I'm missing something, but why not just create an object type, say NSNumber, as a container to your non-object type variable, such as CGFloat?

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


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

I also wanted to do this, but with a method that receives a BOOL parameter. Wrapping the bool value with NSNumber, FAILED TO PASS THE VALUE. I have no idea why.

So I ended up doing a simple hack. I put the required parameter in another dummy function and call that function using the performSelector, where withObject = nil;

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


-(void)dummyCaller {


[self myFunction:YES];


}

Calling performSelector with an NSNumber or other NSValue will not work. Instead of using the value of the NSValue/NSNumber, it will effectively cast the pointer to an int, float, or whatever and use that.

But the solution is simple and obvious. Create the NSInvocation and call

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

DO NOT USE THIS ANSWER. I HAVE ONLY LEFT IT FOR HISTORICAL PURPOSES. SEE THE COMMENTS BELOW.

There is a simple trick if it is a BOOL parameter.

Pass nil for NO and self for YES. nil is cast to the BOOL value of NO. self is cast to the BOOL value of YES.

This approach breaks down if it is anything other than a BOOL parameter.

Assuming self is a 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];

I know this is an old question but if you are building iOS SDK 4+ then you can use blocks to do this with very little effort and make it more readable:

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

I find that the quickest (but somewhat dirty) way to do this is by invoking objc_msgSend directly. However, it's dangerous to invoke it directly because you need to read the documentation and make sure that you're using the correct variant for the type of return value and because objc_msgSend is defined as vararg for compiler convenience but is actually implemented as fast assembly glue. Here's some code used to call a delegate method -[delegate integerDidChange:] that takes a single integer argument.

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

This first saves the selector since we're going to refer to it multiple times and it would be easy to create a typo. It then verifies that the delegate actually responds to the selector - it might be an optional protocol. It then creates a function pointer type that specifies the actual signature of the selector. Keep in mind that all Objective-C messages have two hidden first arguments, the object being messaged and the selector being sent. Then we create a function pointer of the appropriate type and set it to point to the underlying objc_msgSend function. Keep in mind that if the return value is a float or struct, you need to use a different variant of objc_msgSend. Finally, send the message using the same machinery that Objective-C uses under the sheets.

Blocks are the way to go. You can have complex parameters, type safety, and it's a lot simpler and safer than most of the old answers here. For example, you could just write:

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

Blocks allow you to capture arbitrary parameter lists, reference objects and variables.

Backing Implementation (basic):

@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

Example:

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

Also see Michael's answer (+1) for another example.

PerformSelector:WithObject always takes an object, so in order to pass arguments like int/double/float etc..... You can use something like this.

//NSNumber is an object..


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


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


[txtUsername setAlpha: [number floatValue] ];
}

Same way you can use [NSNumber numberWithInt:] etc.... and in the receiving method you can convert the number into your format as [number int] or [number double].

I would always recomend that you use NSMutableArray as the object to pass on. This is because you can then pass several objects, like the button pressed and other values. NSNumber, NSInteger and NSString are just containers of some value. Make sure that when you get the object from the array that you refer to to a correct container type. You need to pass on NS containers. There you may test the value. Remember that containers use isEqual when values are compared.

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