Call a Method from a String in Swift

Call a method from a String in Swift

In Swift, you should use closures and change your approach.
However, if you want to use performSelector to dynamically call a method given only it's String signature, altough it's not supported natively, I've found how to do it.

It is possible to create a C alternative to performSelector that:

  • works even on native swift classes (non objective-c)
  • takes a selector from string

However it's not so straightforward to implement a complete version of it, and it's necessary to create the method in C.

in C we have dlsym(), a function that returns a pointer to a function given the char symbol.
Well, reading this interesting post:
http://www.eswick.com/2014/06/inside-swift/
I've learned a lot of interesting things about swift.

Swift instance methods are plain functions with a specific signature, like this

_TFC14FirstSwiftTest12ASampleClass13aTestFunctionfS0_FT_CSo8NSString

where the "self" value is passed as the last parameter

in short you can call it directly from the c side without any kind of bridging, it is sufficient to rebuild the correct function signature.
In the signature above, there is the name of the project (FirstSwiftTest) and the lenght (14), the name of the class (ASampleClass) and the lenght (12), the name of the function (aTestFunction) and the lenght (13), then other values as the return type ecc ecc. For other details look at the previous link

The function above, is the representation of this:

class ASampleClass
{
func aTestFunction() -> NSString
{
println("called correctly")
return NSString(string: "test")
}

}

Well, on the c side, I was able to create this function

#include <stdio.h>
#include <dlfcn.h>

typedef struct objc_object *id;

id _performMethod(id stringMethod, id onObject)
{
// ...
// here the code (to be created) to translate stringMethod in _TFC14FirstSwiftTest12ASampleClass13aTestFunctionfS0_FT_CSo8NSString
// ...

id (*functionImplementation)(id);
*(void **) (&functionImplementation) = dlsym(RTLD_DEFAULT, "_TFC14FirstSwiftTest12ASampleClass13aTestFunctionfS0_FT_CSo8NSString");

char *error;

if ((error = dlerror()) != NULL) {
printf("Method not found \n");
} else {
return functionImplementation(onObject); // <--- call the function
}
return NULL
}

And then called it on the swift side

    let sampleClassInstance = ASampleClass()

println(_performMethod("aTestFunction", sampleClassInstance))

The function resulted in these statement printed on the log:

called correctly

test

So it should be not so difficult to create a _performMethod() alternative in C that:

  • creates automatically the function signature (since it seems to have a logic :-)
  • manages different return value types and parameters

EDIT

In Swift 2 (and maybe in Beta3, I didn't try) It seems that performSelector() is permitted (and you can call it only on NSObject subclasses). Examining the binary, It seems that now Swift creates static functions that can be specifically called by performSelector.

I created this class

class TestClass: NSObject {
func test() -> Void {
print("Hello");
}
}

let test = TestClass()
let aSel : Selector = NSSelectorFromString("test")
test.performSelector(aSel)

and now in the binary I find

000000010026d830 t __TToFC7Perform9TestClass4testfT_T_

At this time, I don't understand well the reasons behind this, but I'll investigate further

How to call a method in Swift 5 at runtime given a string representation of the method name?

Based on the response of others and more digging, this cannot currently be done using Swift 5.x or later! If you too are trying t do this, you will need to find another solution.

Calling function by name / string in swift

Try to change it to @objc func getDocumentsFolder() -> String

Swift language uses Table Dispatch for all class methods. But for performing selector on method, this method should be invoked with Message Dispatch.

That's what @objc keyword is doing - it's marks function as dynamic and it will be invoked with Message Dispatch

You can read more about method dispatches here

UPD

Agree with @Hamish comment, @objc will not make this function method dispatch for Swift, but for perform(_:) method.

Call function as string in Swift

You should add an @objc inference to the function, otherwise your function will not be expressable in Objective-C, which is what NSSelectorFromString uses.

@objc func function() {
[...]
}

Learn more about the limited @objc inference in Swift 4 here.

(Reflection) Calling A Method With Parameters By Function Name In Swift

First of all, as you noted Swift doesn't have full reflection capabilities and rely on the coexisting ObjC to provide these features.

So even if you can write pure Swift code, you will need Solution to be a subclass of NSObject (or implement NSObjectProtocol).

Playground sample:

class Solution: NSObject {

@objc func functionName(greeting: String, name: String) {
print(greeting, name)
}

}

let solutionInstance = Solution() as NSObject
let selector = #selector(Solution.functionName)
if solutionInstance.responds(to: selector) {
solutionInstance.perform(selector, with: "Hello", with: "solution")
}

There are other points of concern here:

  • Swift's perform is limited to 2 parameters
  • you need to have the exact signature of the method (#selector here)

If you can stick an array in the first parameters, and alway have the same signature then you're done.
But if you really need to go further you have no choice than to go with ObjC, which doesn't work in Playground.

You could create a Driver.m file of the like:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

id call (NSObject *callOn, NSString *callMethod, NSArray <NSObject *>*callParameters)
{
void *result = NULL;
unsigned int index, count;

Method *methods = class_copyMethodList(callOn.class, &count);
for (index = 0; index < count; ++index)
{
Method method = methods[index];

struct objc_method_description *description = method_getDescription(method);
NSString *name = [NSString stringWithUTF8String:sel_getName(description->name)];
if ([name isEqualToString:callMethod])
{
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:description->types];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

NSObject *parameters[callParameters.count];
for (int p = 0; p < callParameters.count; ++p) {
parameters[p] = [callParameters objectAtIndex:p];
[invocation setArgument:¶meters[p] atIndex:p + 2]; // 0 is self 1 is SEL
}
[invocation setTarget:callOn];
[invocation setSelector:description->name];
[invocation invoke];
[invocation getReturnValue:&result];
break;
}
}
free(methods);

return (__bridge id)result;
}

Add it to a bridging-header (for Swift to know about what is in ObjC):

// YourProjectName-Bridging-Header.h
id call (NSObject *callOn, NSString *callMethod, NSArray *callParameters);

And call it with a Solution.swift like this:

import Foundation

class Solution: NSObject {

override init() {
super.init()
// this should go in Driver.swift
let result = call(self, "functionNameWithGreeting:name:", ["Hello", "solution"])
print(result as Any)
}

@objc
func functionName(greeting: String, name: String) -> String {
print(greeting, name)
return "return"
}

}

output:

Hello solution
Optional(return)

Edit: compilation

To compile both ObjC and Swift on the command line you can first compile ObjC to an object file:

$ cc -O -c YouObjCFile.m

Then compile your Swift project with the bridging header and the object file:

$ swiftc -import-objc-header ../Your-Bridging-Header.h YouObjCFile.o AllYourSwiftFiles.swift -o program

working sample

Calling NSString method on a String in Swift

After doing some research, it looks like containsString is not a String function, but can be accessed by bridging to an NSString.

Under Apple's Documentation on Using Swift with Cocoa and Objective-C, it says that

Swift automatically bridges between the String type and the NSString
class. This means that anywhere you use an NSString object, you can
use a Swift String type instead and gain the benefits of both types

But it appears that only some of NSString's functions are accessible without explicitly bridging. To bridge to an NSString and use any of its functions, the following methods work:

 //Example Swift String var
var newString:String = "this is a string"

//Bridging to NSString
//1
(newString as NSString).containsString("string")
//2
newString.bridgeToObjectiveC().containsString("string")
//3
NSString(string: newString).containsString("string")

All three of these work.
It's interesting to see that only some NSString methods are available to Strings and others need explicit bridging. This may be something that is built upon as Swift develops.

A way to dynamically invoke methods in Swift

You can use KVO pattern to solve this issue, however as @David Pasztor comment says.

When you are trying to dynamically call methods in a strongly typed, compiled language, you are most probably on the wrong track to solve your issue. This is especially true for Swift. Explain the problem you're trying to solve that you think requires having to dynamically all methods, rather than trying to translate JS code line by line to Swift.

However KVO can be used to solve such issues, i suggest reading about it it allows you to dynamically observe keys, and based on that you can do your actions.

Keep in mind KVO is compiled with Ojbc compiler.

How do I call a function with the following completion handler?

There can be multiple forms to call the method.

1. Define the parameters when calling the closure, u.e.

InAppHandler.purchaseSubscription(productId: "test") {(x, y, z) in
print(x, y, z)
processPurchase()
}

2. You can use the shorthand form ($0, $1 etc.) for the parameters in the closure while calling it, i.e.

InAppHandler.purchaseSubscription(productId: "test") {
print($0, $1, $2)
processPurchase()
}

The above 2 are same. Just that in first one you're giving the parameter names and in the second one you're using the shorthand for those parameters.

3. In case you're not using any parameters that you're getting in the closure, mark them with underscore(_) like so,

InAppHandler.purchaseSubscription(productId: "test") {(_, _, _) in
processPurchase()
}

You can use any of the other forms depending on your requirement.



Related Topics



Leave a reply



Submit