How to use performSelector:withObject:afterDelay: with primitive types in Cocoa?
Asked Answered
G

13

97

The NSObject method performSelector:withObject:afterDelay: allows me to invoke a method on the object with an object argument after a certain time. It cannot be used for methods with a non-object argument (e.g. ints, floats, structs, non-object pointers, etc.).

What is the simplest way to achieve the same thing with a method with a non-object argument? I know that for regular performSelector:withObject:, the solution is to use NSInvocation (which by the way is really complicated). But I don't know how to handle the "delay" part.

Thanks,

Gillies answered 24/5, 2009 at 19:46 Comment(1)
It's a bit hackish but I find it useful to write fast code. Something like: id object= [array performSelector: @selector(objectAtIndex:) withObject: (__bridge_transfer)(void*)1]; . If the argument is 0 you don't even need a bridge cast because 0 is "special" and id is compatible with it.Indecisive
C
72

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];
Cumbersome answered 14/11, 2009 at 17:58 Comment(3)
Why not just do [theMovie setOrientation: UIInterfaceOrientationPortrait animated:NO]? Or do you mean that the invoke message is in the method you perform-after-delay?Thing
Ah ya, I forgot to say.. `[anInvocation performSelector:@selector(invoke) afterDelay:0.5];Cumbersome
just a side note for those who dont know, we start adding arguments from index 2 because index 0 and 1 are reserved for hidden arguments "self" and "_cmd".Vevina
L
35

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.

Lateshalatest answered 24/5, 2009 at 19:54 Comment(9)
Yea but that would require changing the method signature, which I cannot do.Gillies
Create a wrapper method, -delayedMethodWithArgument:(NSNumber*)arg. Have it call the original method after pulling the primitive out of the NSNumber.Stannic
Understood. Then I'm not sure of an elegant sollution, but you could add another method which is intended as the target of your performSelector:, which unpacks the NSNumber and performs the final selector on the method you currently have in mind. Another idea you could explore is to make a category on NSObject which adds perforSelector:withInt:... (and similar).Lateshalatest
NSValue (the superclass of NSNumber) will wrap anything you give it. If you want to wrap an instance of 'struct foo' called 'bar', you'd do '[NSValue valueWithBytes: &bar objCType: @encode(struct foo)];'Saiga
You can use NSValue to wrap a struct. Either way, NSNumber/NSValue is the correct solution.Thing
@Jim Dovey, can you use NSValue to wrap a typedef enum, like UIKeyboardType? Like, could I do: UIKeyboardType keyboardType = UIKeyboardTypeASCIICapable; NSValue *keyboardTypeObject = [[NSValue alloc] initWithBytes:&keyboardType objCType:@encode(UIKeyboardType)]. See #6130815Cantonese
Absolutely you could, although it might be better to cast the value to a type such as int to better handle cross-platform or compiler differences. Enums in particular can be a little weird-- some compilers will make all enums int or unsigned int, while others will make it the smallest type that can hold all its values.Saiga
I am passing [NSNumber numberWithBool:NO] but the receiving end gets YES. I guess I have to pass nilBascio
Confirmed: MUST pass nil for a BOOL parameter to receive NO (FALSE)Bascio
C
13

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];
Conjugation answered 21/10, 2011 at 15:26 Comment(8)
I believe that BOOL values should always be 0 or 1. It is true that most code won't care and will simply do an if () test on it, but it is conceivable that some code will depend on it being 0 or 1, and thus won't treat some large address value as being the same as 1 (YES).Gillies
Thanks for that, didn't want to create a wrapper or write ugly GCD syntax to do that.Mayer
DON'T USE IT ANYONE. This approach is the source of serious bugs, hard to find. Imagine self with address like 0x0123400. With this approch, You'll get NO insted of YES in 0.4% of cases. Worser, with this probability, this solution may pass all the tests and reveals later.Palatalized
UPD: One would get NO insted of YES with 6% probabiliy because of memory alignment.Palatalized
self with address like 0x0123400 will give NO?? why?@OlegTrakhmanVevina
@iVishal Check out BOOL’s sharp cornersBimetallic
performSelector:selector withObject:(myBool ? @(YES) : nil)Casimiracasimire
@Casimiracasimire that still leaves you open to the memory alignment issue, as you are sending an NSNumber pointer to setHidden, not the YES that it is wrapping.Thievish
B
8

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

Bellied answered 24/5, 2009 at 20:1 Comment(1)
Will any old method "unbox" an NSValue (if possible) to the expected type?Attempt
R
8

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];     
});
Rudolph answered 19/3, 2012 at 16:37 Comment(2)
This creates a retain loop. For this to work properly you need to make a weak reference to self and using that reference to perform the selector. "__block typeof(self) weakSelf = self;"Transept
You don't need a weak reference here because self doesn't retain the block (there is no retain loop). It's probably preferable to use a strong reference actually since self might get deallocated otherwise. See: https://mcmap.net/q/219050/-ios-blocks-and-strong-weak-references-to-selfPalazzo
D
6

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].

Didi answered 21/8, 2013 at 10:35 Comment(1)
If one is restricted to +performSelector:withObject:+, then this is the only right answer posted, IMO. But @MichaelGaylord's answer using blocks is cleaner, if that's an option for you.Engelhardt
S
5

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.

Singlefoot answered 12/7, 2013 at 8:52 Comment(0)
V
3

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]];
    }
}
Villosity answered 17/3, 2014 at 1:22 Comment(0)
E
2

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

}
Eugenol answered 22/2, 2011 at 19:3 Comment(3)
See my comment above on harms' answer. Looks like, since the -performSelector[...] method expects an object (instead of a primitive data type), and it doesn't know that the called selector accepts a boolean, perhaps the address (pointer value) of the NSNumber instance is blindly 'cast' to BOOL (= non-zero, i.e. TRUE ). Perhaps the runtime could be smarter than this and recognize a zero boolean wrapped in an NSNumber!Bascio
I agree. I guess a better thing to do is to avoid BOOL altogether and use integer 0 for NO and 1 for YES, and wrap that with NSNumber or NSValue.Eugenol
Does that change anything? If so, suddenly I am truly disappointed with Cocoa :)Bascio
L
2

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.

Longdistance answered 23/4, 2012 at 23:17 Comment(2)
"Keep in mind that if you're sending floats or structs..." you mean returning floats or structsGillies
Those three lines give you type-safety and guarantee that the number of arguments is what your selector expects. objc_msgSend is a vararg function that's actually implemented as assembly glue so if you don't typecast, you can give it anything.Longdistance
G
1

You Could just use NSTimer to call a selector:

[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(yourMethod:) userInfo:nil repeats:NO]
Guntar answered 11/1, 2010 at 23:21 Comment(2)
You're missing the argument. The argument to yourMethod: in your example is the timer itself, and you haven't passed anything for userInfo. Even if you did use userInfo, you would still have to box up the value, as harms and Remus Rusanu suggested.Thing
-1 for not using performSelector and creating un-necessary NSTimer instanceVolitant
B
1

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]

Bentonbentonite answered 25/2, 2011 at 16:8 Comment(0)
A
0

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];
Accentual answered 27/7, 2010 at 15:13 Comment(2)
The question is about passing primitive types, not NSNumber objects.Thousandfold
The question supposes that the OP only has external access to the method and cannot change the signature.Attempt

© 2022 - 2024 — McMap. All rights reserved.