iOS - How to implement a performSelector with multiple arguments and with afterDelay?
Asked Answered
L

11

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?

Linnette answered 8/12, 2011 at 23:11 Comment(0)
I
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];
}
Ironstone answered 2/4, 2013 at 10:11 Comment(7)
Agreed. It should be the correct answer. Very helpful solution. Specially in my case where it is not allowed to changed the signature of the method containing multiple arguments.Faqir
This looks like a great solution. Is there a way to get a return value from the method being invoked with this technique?Evalyn
How do you specify the delay with this technique?Amendment
@death_au, just instead of invoke, call: [inv performSelector:@selector(invoke) withObject:nil afterDelay:1]; I must agree this is a great solution. Happy coding everyone!Oleomargarine
Kind of late to the conversation, but have a question. What is dropDownDelegate?Ilene
I am guessing that is the target class which implements dropDownSelectedRow:withDataSource:Northnortheast
what would happen if you just set 1 argument and not the other one for dropDownSelectedRow:withDataSource:Northnortheast
I
96

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];
Illegible answered 8/12, 2011 at 23:14 Comment(7)
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?Linnette
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. :-)Illegible
How do I pass Bool type here?Joule
Make it an Objective C style object (e.g. "NSNumber * whatToDoNumber = [NSNumber numberWithBool: doThis];") and pass it along as the one parameter, @virata.Illegible
Can any one tell me how to change the label text dynamically based on the array value ?Valvate
that's a separate question @Valvate ... please post it separately.Illegible
@Linnette 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.Tisdale
H
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:

Heyes answered 8/12, 2011 at 23:25 Comment(2)
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.Labonte
Yes this should be the accepted answer. Its more appropriate and straight forward.Mariel
S
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];
Sternick answered 8/12, 2011 at 23:23 Comment(2)
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.Swat
@Swat - 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.Sternick
C
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];
Callimachus answered 8/12, 2011 at 23:21 Comment(0)
H
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: 
Hydromechanics answered 8/12, 2011 at 23:24 Comment(0)
T
3

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);
Towhee answered 31/10, 2016 at 9:47 Comment(1)
Nice! Replace the 'vc' with 'target'Kitten
V
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").

Vermouth answered 27/3, 2013 at 9:41 Comment(0)
J
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];
}
Jehol answered 2/5, 2019 at 14:51 Comment(0)
B
0

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

Blithering answered 2/5, 2019 at 14:55 Comment(0)
E
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
}
Ethiopian answered 10/4, 2023 at 5:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.