objective C using a string to call a method
Asked Answered
F

4

5

Hi im new to objective C and was wondering if someone might be able to help me with this. I have a few different methods each requiring 3 input values and normally call it using

[self methodA:1 height:10 speed:3]

but the method name I want to read from a string in a plist so for example if the string was methodB i would get

[self methodB:1 height:10 speed:3] 

for "methodC"

[self methodC:1 height:10 speed:3]

and so on.

Any ideas how I might do this I tried defining the string as a Selector using NSSelectorFromString

NSString *string = [plistA objectForKey:@"method"];
SEL select = NSSelectorFromString(string);
[self performSelector:select:c height:b speed:a]; 

However this did not work either any help would be greatly appreciated. Have tried the solution below but could not get to work here is what i've tried.

So just to recap I have methods such as

 spawnEnemyA:2 withHeight:3 withSpeed:4  
 spawnEnemyB:3 withHeight:2 withSpeed:5 

and I want to read the values I want to pass to these methods as well as the method type from a plist file. my code is as follows, //////////////////////////////////////////////////////////////

//These are the values I read from the plist that I want my method to use

    int a = [[enemySettings objectForKey:@"speed"] intValue];
    int b = [[enemySettings objectForKey:@"position"] intValue];
    int c = [[enemySettings objectForKey:@"delay"] intValue];

   // I Also read the method name from the plist and combine it into a single string  
    NSString *method = [enemySettings objectForKey:@"enemytype"];
    NSString *label1 = @"spawn";
    NSString *label2 = @":withHeight:withSpeed:";
    NSString *combined = [NSString stringWithFormat:@"%@%@%@",label1, method,label2];


    //Check that the string is correct get spawnEnemyA:withHeight:withSpeed:
    CCLOG(@"%@",combined);


//This is the Invocation part 
    NSInvocation * invocation = [ NSInvocation new ];

    [ invocation setSelector: NSSelectorFromString(combined)];
    [ invocation setArgument: &c atIndex: 2 ];
    [ invocation setArgument: &b atIndex: 3 ];
    [ invocation setArgument: &a atIndex: 4 ];

    [ invocation invokeWithTarget:self ];

    [invocation release ];

////////////////////////////////////////////////////////////////////

The code compiles without any errors but the methods are not called. Any ideas? Cheers

Fencing answered 20/6, 2012 at 18:14 Comment(0)
G
12

You can't use performSelector for a method with 3 (or more) arguments.
But for your information, here's how to use it:

SEL m1;
SEL m2;
SEL m3;

m1 = NSSelectorFromString( @"someMethodWithoutArg" );
m2 = NSSelectorFromString( @"someMethodWithAnArg:" );
m1 = NSSelectorFromString( @"someMethodWithAnArg:andAnotherOne:" );

[ someObject performSelector: m1 ];
[ someObject performSelector: m2 withObject: anArg ];
[ someObject performSelector: m2 withObject: anArg withObject: anOtherArg ];

For methods with more than 2 arguments, you will have to use the NSInvocation class.
Take a look at the documentation to learn how to use it.

Basically:

NSInvocation * invocation = [ NSInvocation new ];

[ invocation setSelector: NSStringFromSelector( @"methodWithArg1:arg2:arg3:" ) ];

// Argument 1 is at index 2, as there is self and _cmd before
[ invocation setArgument: &arg1 atIndex: 2 ];
[ invocation setArgument: &arg2 atIndex: 3 ];
[ invocation setArgument: &arg3 atIndex: 4 ];

[ invocation invokeWithTarget: targetObject ];

// If you need to get the return value
[ invocation getReturnValue: &someVar ];

[ invocation release ];
Gynandrous answered 20/6, 2012 at 18:21 Comment(10)
A fan of spaces, I see. :) This'll work, too. For calls like these, I prefer the casted objc_msgSend() for both speed and, most importantly, type safety in that the compiler will complain if I screw up the types later.Prodrome
I totally agree about speed. But for a beginner, I think the Objective-C way may be a bit less confusing, even if it's pretty important to know how the runtime works. I don't agree about type safety, as IMHO, both ways are type unsafe. That's the purpose of objc_msgSend. Won't be generic otherwise. Anyway, thanks for your comment. : ) (and btw, yes, I love spaces!)Gynandrous
Actually, the cast objc_msgSend() is considerably more type safe assuming you get the cast correct. The arguments to the invocation are, effectively, untyped. And that code is wrong; you would need to pass the address of the arguments (which I didn't catch the first read)! All that pointer magic goop certainly adds a lot of complexity.Prodrome
Corrected the setArgument issue, thanks to point it out. : ) About type safety, you say: «assuming the cast is correct». This is not really type safety, as you can just cast to any type. That kind of cast just tells the compiler to shut-up. About arguments, as the objc_msgSend prototype has ..., you can also pass everything (also because objc_msgSend is actually implemented using assembly, and then doesn't care much about types). You add some safety in your example by casting the function pointer. This is clever, but I don't think it's safer than NSInvocation...Gynandrous
And I don't know if it's really the place for that kind of discussion, even if I would surely envoy such an argumentation : DGynandrous
But that is the issue... there is no way that the argumentation to setArgument:atIndex: can ever be vetted for correctness by the compiler! Sure, the cast is fragile, but at least it exists! If you tried to pass a double as the 3rd argument to the casted call, the compiler will complain mightily. And, in fact, under ARC, you can no longer call the varargs form of objc_msgSend() without warning that you need to cast it to specific type.Prodrome
Then I guess we agree : ) (and don't tell me about ARC) : PGynandrous
Given it a try and not been able to get it working it doesn't throw any errors any ideas how I could make it more verbose to try and find whats going on? CheersFencing
Please post the code you tried, so I'll be able to help out... : )Gynandrous
Sorry Ive taken so long to reply was away for the weekend. Have added the code to the original question. Cheers @GynandrousFencing
P
4

In general, this kind of dynamism often indicates an anti-pattern. Colluding data with implementation in this fashion is not generally a best practice.

Sometimes, though, it is necessary. If you are going down this path, then given that your various method declarations likely look like:

- (void)methodAWidth:(NSUInteger)w height:(NSUInteger)h speed:(NSUInteger)s;
- (void)methodBWidth:(NSUInteger)w height:(NSUInteger)h speed:(NSUInteger)s;
- (void)methodCWidth:(NSUInteger)w height:(NSUInteger)h speed:(NSUInteger)s;

You would probably want something like:

NSString *selName = [NSString stringWithFormat:@"method%@Width:height:speed:", ... one of @"A", @"B", or @"C" ....];
SEL selector = NSelectorFromString(selName);

Then:

if (![target respondsToSelector:selector])
    return; // no can do

void (*castMsgSend)(id, SEL, NSUInteger, NSUInteger, NSUInteger) = (void*)objc_msgSend;
castMsgSend(target, selector, 1, 10, 3);

Every method call is compiled down to a call to objc_msgSend(). By doing the above, you are creating a fully type-safe/type-checked call site that goes through the normal Objective-C messaging mechanism, but the selector is dynamically defined on the fly.\

While performSelector: (and multi-arg variants) are handy, they can't deal with non-object types.


And, as MacMade pointed out in a comment, watch out for floating point and structure returns. They use different variants of objc_msgSend() that the compiler automatically handles in the normal [foo bar] case.

Prodrome answered 20/6, 2012 at 18:32 Comment(1)
When i try this I get the following error message for the void (castMSgSend) part Cannot initialize a variable of type 'void()(id, SEL, NSUInteger, NSUInteger,NSUInteger)' with an rvalue of type 'void *' How can I fix this? Cheers @ProdromeFencing
K
1

You can directly use objc_msgsend:

NSString *methodName = [plistA objectForKey:@"method"];
objc_msgSend(self, methodName, c, b, a);

Mind that the selector must include all pieces, eg @"method:height:speed:"

Kaete answered 20/6, 2012 at 18:19 Comment(3)
You need to cast the call to objc_msgSend() for this to be correct.Prodrome
Maybe a bit low-level for a beginner ; ) Also keep in mind that, if you need a return value, there is also objc_msgSend_fpret (floating point return) and objc_msgSend_stret (struct return).Gynandrous
Yes, I can imagine that it can be somewhat lowlevel, but at least you can learn how the Obj-C is built on C :) Take a look here for more details on this method: #2574305Kaete
C
-1

You should replace the following line with:

[self performSelector:select:c height:b speed:a];

and write following:

[self performSelector:select withObject:[NSArray arrayWithObjects:c,b,a,nil]];
Carriole answered 20/6, 2012 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.