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

Context
I have an instance of class called Solution and I have a function name as a string functionName that I want to call on the Solution instance solutionInstance. I have the parameters for the function in an array and I'd like to pass those as well.
I am using the Swift compiler to compile all of my .swift files together (swiftc with a files enumerated and then -o and the output file name) then I run the final output.
Python Example
Here is how I do this in Python:
method = getattr(solutionInstance, functionName) # get method off of instance for function
programOutput = method(*testInputsParsed) # pass the list of parameters & call the method
Purpose
This is server-side code that runs in a container to run a user's code. This code lives in a "Driver" main.swift file that calls the methods and orchestrates testing.
Problem
Swift is statically typed and I've been searching around and most sources say there is limited reflection support in Swift (and suggest to "reach into Objective-C" to get the functionality desired).
Swift is not my native language (TypeScript/JavaScript, Java, Python strongest, then C# and C++ mild, then just implementing Swift code for this feature now) so I'm not sure what that means and I haven't been able to find a definitive answer.
Question
How can I call a function by its name on a Solution class instance (it implements no protocols, at least by me) and pass an array of parameters in Swift (using reflection)? How does my setup need to change to make this happen (importing libraries, etc.)
Thank you!
Referenced Posts
Calling Method using reflection
Does Swift support reflection?
Call a method from a String in Swift
How to invoke a class method using performSelector() on AnyClass in Swift?
Dynamically call a function 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:&parameters[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

Related

Objective C - Called object type 'BOOL' (aka 'bool') is not a function or function pointer

Working on a legacy hybrid iOS project. Created one new Swift util class in ConsentManager.swift, like below,
import Foundation
public class ConsentManager: NSObject {
#objc static let sharedInstance = ConsentManager()
#objc private override init() {}
#objc public func isDataPermissionConsentRequired() -> Bool
{
…
return value; // based on logic
}
}
Called the method from another objc class, ConsentChecker.m like,
#interface ConsentChecker ()
{
}
#end
#implementation ConsentChecker
-(void)checkConsent {
// GETTING ERROR IN THE FOLLOWING LINE
if (ConsentManager.sharedInstance.isDataPermissionConsentRequired()) {
…
}
}
#end
Getting compiler error:
Called object type 'BOOL' (aka 'bool') is not a function or function pointer
Why and how to resolve it?
The reason you're hitting this is that methods in Objective-C which take no arguments may be called implicitly using dot syntax similar to Swift's, but not exactly like it. A method declared like
// Inside of SomeClass
- (BOOL)someMethod { /* return something */ }
can be called either as
SomeClass *instance = ...
// Traditional Obj-C syntax:
BOOL value = [instance someMethod];
or
// Dot syntax accessor:
BOOL value = instance.someMethod;
Note that the dot syntax version does not use parentheses to denote the call. When you add parentheses like you would in Swift, Obj-C determines that you are trying to call the returned value from the method as if it were a function:
instance.someMethod();
// equivalent to:
BOOL value = [instance someMethod];
value(); // <- Called object type 'BOOL' (aka 'bool') is not a function or function pointer
You cannot call a BOOL like you can a function, hence the error.
#Dávid offers the more traditional Obj-C syntax for calling this method, but alternatively, you can simply drop the parentheses from your call:
if (ConsentManager.sharedInstance.isDataPermissionConsentRequired) {
Objective-C-ism note:
Dot syntax is most idiomatically used for method calls which appear like properties (e.g. boolean accessors like your isDataPermissionConsentRequired), even if the method might need to do a little bit of work to return that value (think: computed properties in Swift).
For methods which perform an action, or which return a value but might require a significant amount of work, traditional method call syntax is typically preferred:
// Prefer:
[instance doTheThing];
NSInteger result = [instance performSomeExpensiveCalculation];
// over:
instance.doTheThing;
NSInteger result = instance.performSomeExpensiveCalculation;
The Obj-C syntax for executing methods is different from Swift's dot syntax.
This is the correct syntax:
if ([ConsentManager.sharedInstance isDataPermissionConsentRequired]) {
If u want to call swift function on obj-c class you use to obj-c syntax
Correct Syntax is:
if ([ConsentManager.sharedInstance isDataPermissionConsentRequired]) {
// Write logic here
}

How to Dynamically add XCTestCase

I'm writing a UI Test for a white label project where each app has a different set of menu items. The test taps on each menu item and takes a screenshot (using fastlane snapshot).
Currently this all happens inside one XCTestCase called testScreenshotAllMenuItems() which looks like this:
func testScreenshotAllMenuItems() {
// Take a screenshot of the menu
openTheMenu()
snapshot("Menu")
var cells:[XCUIElement] = []
// Store each menu item for use later
for i in 0..<app.tables.cells.count {
cells.append(app.tables.cells.element(boundBy: i))
}
// Loop through each menu item
for menuItem in cells.enumerated() {
let exists = menuItem.element.waitForExistence(timeout: 5)
if exists && menuItem.element.isHittable {
// Only tap on the menu item if it isn't an external link
let externalLink = menuItem.element.children(matching: .image)["external link"]
if !externalLink.exists {
var name = "\(menuItem.offset)"
let cellText = menuItem.element.children(matching: .staticText).firstMatch
if cellText.label != "" {
name += "-\(cellText.label.replacingOccurrences(of: " ", with: "-"))"
}
print("opening \(name)")
menuItem.element.tap()
// Screenshot this view and then re-open the menu
snapshot(name)
openTheMenu()
}
}
}
}
I'd like to be able to dynamically generate each screenshot as it's own test case so that these will be reported correctly as individual tests, maybe something like:
[T] Screenshots
[t] testFavouritesViewScreenShot() ✓
[t] testGiveFeedbackViewScreenShot() ✓
[t] testSettingsViewScreenShot() ✓
I've had a look at the documentation on creating tests programmatically but I'm not sure how to set this up in a swifty fashion. - Ideally I would use closures to wrap the existing screenshot tests in to their own XCTestCase - I imagined this like the following but there doesn't appear to be any helpful init methods to make this happen:
for menuItem in cells {
let test = XCTestCase(closure: {
menuItem.tap()
snapshot("menuItemName")
})
test.run()
}
I don't understand the combination of invocations and selectors that the documentation suggests using and I can't find any good examples, please point me in the right direction and or share any examples you have of this working.
You probably can't do it in pure swift since NSInvocation is not part of swift api anymore.
XCTest rely on + (NSArray<NSInvocation *> *)testInvocations function to get list of test methods inside one XCTestCase class. Default implementation as you can assume just find all methods that starts with test prefix and return them wrapped in NSInvocation. (You could read more about NSInvocation here)
So if we want to have tests declared in runtime, this is point of interest for us.
Unfortunately NSInvocation is not part of swift api anymore and we cannot override this method.
If you OK to use little bit of ObjC then we can create super class that hide NSInvocation details inside and provide swift-friendly api for subclasses.
/// Parent.h
/// SEL is just pointer on C struct so we cannot put it inside of NSArray.
/// Instead we use this class as wrapper.
#interface _QuickSelectorWrapper : NSObject
- (instancetype)initWithSelector:(SEL)selector;
#end
#interface ParametrizedTestCase : XCTestCase
/// List of test methods to call. By default return nothing
+ (NSArray<_QuickSelectorWrapper *> *)_qck_testMethodSelectors;
#end
/// Parent.m
#include "Parent.h"
#interface _QuickSelectorWrapper ()
#property(nonatomic, assign) SEL selector;
#end
#implementation _QuickSelectorWrapper
- (instancetype)initWithSelector:(SEL)selector {
self = [super init];
_selector = selector;
return self;
}
#end
#implementation ParametrizedTestCase
+ (NSArray<NSInvocation *> *)testInvocations {
// here we take list of test selectors from subclass
NSArray<_QuickSelectorWrapper *> *wrappers = [self _qck_testMethodSelectors];
NSMutableArray<NSInvocation *> *invocations = [NSMutableArray arrayWithCapacity:wrappers.count];
// And wrap them in NSInvocation as XCTest api require
for (_QuickSelectorWrapper *wrapper in wrappers) {
SEL selector = wrapper.selector;
NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = selector;
[invocations addObject:invocation];
}
/// If you want to mix parametrized test with normal `test_something` then you need to call super and append his invocations as well.
/// Otherwise `test`-prefixed methods will be ignored
return invocations;
}
+ (NSArray<_QuickSelectorWrapper *> *)_qck_testMethodSelectors {
return #[];
}
#end
So now our swift test classes need to just inherit from this class and override _qck_testMethodSelectors:
/// RuntimeTests.swift
class RuntimeTests: ParametrizedTestCase {
/// This is our parametrized method. For this example it just print out parameter value
func p(_ s: String) {
print("Magic: \(s)")
}
override class func _qck_testMethodSelectors() -> [_QuickSelectorWrapper] {
/// For this example we create 3 runtime tests "test_a", "test_b" and "test_c" with corresponding parameter
return ["a", "b", "c"].map { parameter in
/// first we wrap our test method in block that takes TestCase instance
let block: #convention(block) (RuntimeTests) -> Void = { $0.p(parameter) }
/// with help of ObjC runtime we add new test method to class
let implementation = imp_implementationWithBlock(block)
let selectorName = "test_\(parameter)"
let selector = NSSelectorFromString(selectorName)
class_addMethod(self, selector, implementation, "v#:")
/// and return wrapped selector on new created method
return _QuickSelectorWrapper(selector: selector)
}
}
}
Expected output:
Test Suite 'RuntimeTests' started at 2019-03-17 06:09:24.150
Test Case '-[ProtocolUnitTests.RuntimeTests test_a]' started.
Magic: a
Test Case '-[ProtocolUnitTests.RuntimeTests test_a]' passed (0.006 seconds).
Test Case '-[ProtocolUnitTests.RuntimeTests test_b]' started.
Magic: b
Test Case '-[ProtocolUnitTests.RuntimeTests test_b]' passed (0.001 seconds).
Test Case '-[ProtocolUnitTests.RuntimeTests test_c]' started.
Magic: c
Test Case '-[ProtocolUnitTests.RuntimeTests test_c]' passed (0.001 seconds).
Test Suite 'RuntimeTests' passed at 2019-03-17 06:09:24.159.
Kudos to Quick team for super class implementation.
Edit: I created repo with example github

Timing issues with swift class with dynamic variable, which inherits from objective c, where the get/set methods are replaced

I have a very specific issue where, if I have a swift class which inherits from an objective-c class, and it has a dynamic property.
Now, on the +initialize, I am injecting getter and setters into the swift(:NSObject) class, and these work no problem, except for a slight issue when setting values from an init overload.
So, my swift class looks like this:
class TestClass: BaseClass {
dynamic var testStringProperty: String?
init(initialValue: String) {
super.init()
self.testStringProperty = initialValue;
// does not trigger the get/set methods
// a po self.testStringProperty will output 'nil'
let xyz = self.testStringProperty;
// xyz is actually set to the value of initialValue, but it does not trigger the getter.
}
}
And the objective-c class that bases the swift is as follows:
static id storedVar;
#implementation BaseClass
+ (void)initialize {
// we are going to exchange getter/setter methods for a property on a SwiftClass, crude but demonstrates the point
if(![[self description] isEqualToString:#"BaseClass"]) {
IMP reader = (IMP)getPropertyIMP;
IMP writer = (IMP)setPropertyIMP;
const char* type = "#";
NSString* propertyName = #"testStringProperty";
IMP oldMethod = class_replaceMethod([self class], NSSelectorFromString(propertyName), reader, [[NSString stringWithFormat:#"%s#:", type] UTF8String]);
NSString* setMethod = [NSString stringWithFormat:#"set%#%#:", [[propertyName substringToIndex:1] uppercaseString], [propertyName substringFromIndex:1]];
oldMethod = class_replaceMethod([self class], NSSelectorFromString(setMethod), writer, [[NSString stringWithFormat:#"v#:%s",type] UTF8String]);
}
}
static id getPropertyIMP(id self, SEL _cmd) {
return storedVar;
}
static void setPropertyIMP(id self, SEL _cmd, id aValue) {
storedVar = aValue;
}
#end
Long story short, whilst in the call to init(initialValue: String), the getters and setters are not triggered, but immediately after the call to init has completed they work.
This is despite the call to initialize completing successfully and the methods being replaced.
But outside of the init function, the get/set behave as expected.
Here is where it get's called.
let test = TestClass(initialValue: "hello");
test.testStringProperty = "hello"
A po test.testStringProperty after the creation of the object, will output nil. But the subsequent assignment, triggers all the correct methods.
It only fails when assigning within the init. Everywhere else it works like a charm.
I would like to get this to work within the initializer if possible, i'm not sure if there is another way to work around it.
Here is a link to the sample app that replicates the issue:
https://www.dropbox.com/s/5jymj581yps799d/swiftTest.zip?dl=0

Using Swift Shared Instance in Objective C

I have a shared instance of a class in Swift that I'm using in Objective-C. I'm unable to create the shared instance and use the instance function. Here's my Swift code.
class VideoPlayerSignaler: NSObject {
static let sharedInstance = VideoPlayerSignaler()
let playerAction = Signal<PlayerAction>()
private override init() {
}
func firePlayerAction(action: PlayerAction) {
playerAction.fire(action)
}
}
Here's my Objective-C code.
VideoPlayerSignaler *signaler = [VideoPlayerSignaler sharedInstance];
// This is the line that is producing the issue.
// It's as if the signaler variable is a Class Instance
[signaler firePlayerAction: PlayerAction.Stop];
The error I'm producing states that firePlayerAction does not exist. In essence, Objective C believes the signaler variable to be a class instance.
What am I doing wrong and how do I fix it so that signaler is a shared instance of VideoPlayerSignaler?
There's nothing wrong with your Swift, or with how you access the singleton instance from ObjC — the problem is the enum value you're passing to it.
Presumably your enum declaration looks something like this:
enum PlayerAction: Int {
case Stop, Collaborate, Listen // etc
}
To make your enum accessible to ObjC, you need to preface the declaration with #objc:
#objc enum PlayerAction: Int { /*...*/ }
This makes it appear as a Cocoa-style NS_ENUM declaration in ObjC, creating global symbols for case names by concatenating the Swift enum type's name with the case names:
typedef NS_ENUM(NSInteger, PlayerAction) {
PlayerActionStop = 1,
PlayerActionCollaborate,
PlayerActionListen, // etc
};
So those names are what you should be passing when you call a method taking an enum value from ObjC:
[signaler firePlayerAction: PlayerActionStop]; // NOT PlayerAction.Stop
(The only docs I can find to cite for this are buried in the Attributes chapter in The Swift Programming Language — scroll down the to objc attribute.)

Use an NSString to programatically access or create a method in Objective-C

I am trying to use an array of strings dynamically access methods at runtime within my class. For now the methods are already there, eventually I want to create them.
Is this possible?
For example:
bool nextLevel=NO;
for(NSString * match in gameLevels)
{
if([match isEqualToString:self.level])
{
nextLevel=YES;
}
else if(nextLevel==YES)
{
self.level=match;
nextLevel=NO;
}
}
//access method named self.level
Thank you in advance!
I use:
NSSelectorFromString(selectorString)
In your case, the selectorString would be:
NSString * selectorString = #"setLevel:";
This is 'setLevel' instead of 'level' because the Objective-C runtime will automatically expand dot properties to these selector names when assignment occurs.
To access a method based on a string, check the other answer.
To add a method in the runtime you need to create a IMP function or block.
If using a function, could be something like:
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
You could also use a block like this:
IMP blockImplementation=imp_implementationWithBlock(^(id _self, ...){
//Your Code here
}
Then you need to add the method, like this:
class_addMethod(yourClass, #selector(selectorName), (IMP) blockImplementation, encoding);
The encoding part is a special runtime encoding to describe the type of parameters your method receives. You can find that on the Objective-C runtime reference.
If you receive dynamic arguments on your generated methods, you need to use the va_list to read the values.

Resources