91

I am an iOS newbie. I have a selector method as follows -

- (void) fooFirstInput:(NSString*) first secondInput:(NSString*) second
{

}

I am trying to implement something like this -

[self performSelector:@selector(fooFirstInput:secondInput:) withObject:@"first" withObject:@"second" afterDelay:15.0];

But that gives me an error saying -

Instance method -performSelector:withObject:withObject:afterDelay: not found

Any ideas as to what I am missing?

Suchi
  • 9,989
  • 23
  • 68
  • 112

11 Answers11

145

Personally, I think that a closer solution to your needs is the use of NSInvocation.

Something like the following will do the work:

indexPath and dataSource are two instance variables defined in the same method.

SEL aSelector = NSSelectorFromString(@"dropDownSelectedRow:withDataSource:");

if([dropDownDelegate respondsToSelector:aSelector]) {
    NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[dropDownDelegate methodSignatureForSelector:aSelector]];
    [inv setSelector:aSelector];
    [inv setTarget:dropDownDelegate];

    [inv setArgument:&(indexPath) atIndex:2]; //arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
    [inv setArgument:&(dataSource) atIndex:3]; //arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation

    [inv invoke];
}
Dane Jordan
  • 1,071
  • 1
  • 17
  • 27
valvoline
  • 7,737
  • 3
  • 47
  • 52
97

Because there is no such thing as a [NSObject performSelector:withObject:withObject:afterDelay:] method.

You need to encapsulate the data you want to send along into some single Objective C object (e.g. a NSArray, a NSDictionary, some custom Objective C type) and then pass it through the[NSObject performSelector:withObject:afterDelay:] method that is well known and loved.

For example:

NSArray * arrayOfThingsIWantToPassAlong = 
    [NSArray arrayWithObjects: @"first", @"second", nil];

[self performSelector:@selector(fooFirstInput:) 
           withObject:arrayOfThingsIWantToPassAlong  
           afterDelay:15.0];
Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
  • I do not get an error if I remove the afterDelay param. Does that mean afterDelay is not allowed to be used with more than one param? – Suchi Dec 08 '11 at 23:17
  • 1
    you don't get an error, but I'd bet you would get a "selector not found" exception at run time (and the thing you're trying to perform wouldn't get called)... try it and see. :-) – Michael Dautermann Dec 08 '11 at 23:23
  • How do I pass Bool type here? – virata Oct 11 '12 at 06:13
  • Make it an Objective C style object (e.g. "`NSNumber * whatToDoNumber = [NSNumber numberWithBool: doThis];`") and pass it along as the one parameter, @virata. – Michael Dautermann Oct 11 '12 at 12:27
  • Can any one tell me how to change the label text dynamically based on the array value ? – Raj Oct 12 '12 at 05:23
  • 2
    that's a separate question @Raj ... please post it separately. – Michael Dautermann Oct 12 '12 at 11:01
  • @Suchi in response to your Dec 8 question, this is because Apple provides multiple methods, including `performSelector:withObject:`, `performSelector:withObject:withObject:`, `performSelector:withObject:afterDelay:`. Removing `afterDelay` just calls a different method. – Connor Jul 27 '15 at 18:19
34

You can package your parameters into one object and use a helper method to call your original method as Michael, and others now, have suggested.

Another option is dispatch_after, which will take a block and enqueue it at a certain time.

double delayInSeconds = 15.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);

dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

    [self fooFirstInput:first secondInput:second];

});

Or, as you've already discovered, if you don't require the delay you can just use - performSelector:withObject:withObject:

Firoze Lafeer
  • 17,133
  • 4
  • 54
  • 48
  • What's also good about this approach is that you can use `__weak` to give your pretend timer only a weak link back to self — so you don't end up artificially extending your object's lifecycle and e.g. if your performSelector:afterDelay: effects something a little like tail recursion (albeit without the recursion) then it resolves the retain cycle. – Tommy May 24 '13 at 01:15
  • 1
    Yes this should be the accepted answer. Its more appropriate and straight forward. – Roohul Aug 18 '17 at 11:33
7

The simplest option is to modify your method to take a single parameter containing both arguments, such as an NSArray or NSDictionary (or add a second method that takes a single parameter, unpacks it, and calls the first method, and then call the second method on a delay).

For instance, you could have something like:

- (void) fooOneInput:(NSDictionary*) params {
    NSString* param1 = [params objectForKey:@"firstParam"];
    NSString* param2 = [params objectForKey:@"secondParam"];
    [self fooFirstInput:param1 secondInput:param2];
}

And then to call it, you can do:

[self performSelector:@selector(fooOneInput:) 
      withObject:[NSDictionary dictionaryWithObjectsAndKeys: @"first", @"firstParam", @"second", @"secondParam", nil] 
      afterDelay:15.0];
Adil Malik
  • 6,279
  • 7
  • 48
  • 77
aroth
  • 54,026
  • 20
  • 135
  • 176
  • What if the method can't be modified, say it lives in UIKit or something? Not only that, changing the method to use `NSDictionary` loses type safety as well. Not ideal. – fatuhoku Nov 30 '15 at 23:17
  • @fatuhoku - That's covered by the parenthetical; "add a second method that takes a single parameter, unpacks it, and calls the first method". That works _regardless_ of where the first method lives. As for type safety, that was lost the moment the decision was made to use `performSelector:` (or `NSInvocation`). If that's a concern, the best option would probably be to go through GCD. – aroth Nov 30 '15 at 23:50
6
- (void) callFooWithArray: (NSArray *) inputArray
{
    [self fooFirstInput: [inputArray objectAtIndex:0] secondInput: [inputArray objectAtIndex:1]];
}


- (void) fooFirstInput:(NSString*) first secondInput:(NSString*) second
{

}

and call it with:

[self performSelector:@selector(callFooWithArray) withObject:[NSArray arrayWithObjects:@"first", @"second", nil] afterDelay:15.0];
Mike Wallace
  • 276
  • 2
  • 7
5

You can find all the types of provided performSelector: methods here:

http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/nsobject_Class/Reference/Reference.html

There are a bunch of variations but there isn't a version that takes multiple objects as well as a delay. You'll need to wrap up your arguments in an NSArray or NSDictionary instead.

- performSelector:
- performSelector:withObject:
- performSelector:withObject:withObject:
– performSelector:withObject:afterDelay:
– performSelector:withObject:afterDelay:inModes:
– performSelectorOnMainThread:withObject:waitUntilDone:
– performSelectorOnMainThread:withObject:waitUntilDone:modes:
– performSelector:onThread:withObject:waitUntilDone:
– performSelector:onThread:withObject:waitUntilDone:modes:
– performSelectorInBackground:withObject: 
StilesCrisis
  • 15,972
  • 4
  • 39
  • 62
2

I dislike the NSInvocation way, too complex. Let’s keep it simple and clean:

// Assume we have these variables
id target, SEL aSelector, id parameter1, id parameter2;

// Get the method IMP, method is a function pointer here.
id (*method)(id, SEL, id, id) = (void *)[target methodForSelector:aSelector];

// IMP is just a C function, so we can call it directly.
id returnValue = method(target, aSelector, parameter1, parameter2);
BB9z
  • 2,432
  • 1
  • 30
  • 36
1

I just did some swizzling and needed to call the original method. What I did was making a protocol and cast my object to it. Another way is to define the method in a category, but would need suppression of a warning (#pragma clang diagnostic ignored "-Wincomplete-implementation").

darkfader
  • 31
  • 1
0

A simple and reusable way is to extend NSObject and implement

- (void)performSelector:(SEL)aSelector withObjects:(NSArray *)arguments;

something like:

- (void)performSelector:(SEL)aSelector withObjects:(NSArray *)arguments
{
    NSMethodSignature *signature = [self methodSignatureForSelector: aSelector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: signature];
    [invocation setSelector: aSelector];

    int index = 2; //0 and 1 reserved
    for (NSObject *argument in arguments) {
        [invocation setArgument: &argument atIndex: index];
        index ++;
    }
    [invocation invokeWithTarget: self];
}
Kappe
  • 9,217
  • 2
  • 29
  • 41
0

I would just create a custom object holding all my parameters as properties, and then use that single object as the parameter

MobileMon
  • 8,341
  • 5
  • 56
  • 75
0

For Single argument

perform(#selector(fuctionOne(_:)), with: arg1, afterDelay: 2)
@objc func fuctionOne(_ arg1: NSUserActivity) {
    // Do Something
}

For Multiple arguments

perform(#selector(fuctionTwo(_:_:)), with: [arg1, arg2], afterDelay: 2)
@objc func fuctionTwo(_ arg1: URL, _ arg2: [UIApplication.OpenURLOptionsKey: Any] = [:]) {
    // Do Something
}
Divyansh Jain
  • 373
  • 2
  • 5