Is there any alternative for NSInvocation in Swift?
Asked Answered
F

2

8

I'm trying to invoke a selector, with multiple (2+) arguments (the number of arguments can be determined). However, the selector is unknown at compile time (generated with NSSelectorFromString, actually).

In Objective-C, I could create an invocation and set arguments to it and invoke it. But this is not available in Swift. Is there any way around this? Like:

let obj = SomeClass()
let selector = NSSelectorFromString("arg1:arg2:arg3:") //selector, arguments known only at runtime
//invoke selector
Fezzan answered 10/11, 2016 at 17:44 Comment(10)
What exactly is the problem you're trying to solve?Craner
I'm sorry, but I'm working for a client. But I'll try to describe it my best. I have to configure a view with help of a configuration file, which can be configured as required. That said, the list of possible configurations (with each trying to access many different methods) are big. Yes, they can be managed case by case (mapping a case to a method), but I'm trying to use a general purpose solution to try and manage this.Fezzan
This kind of code dynamicity is usually the result of poor architecture (no offense). There are some very specific exceptions but in 95% you don't need to use NSInvocation and you shouldn't, even in objective-C. One possible solution are named closures in a dictionary but even that is a bit of a code smell.Tufts
None taken, but why do you think that such taking advantage of this dynamism is bad? For me, lowers coding burden and with proper checks, I think it could have been very versatile. FYI, I'm not saying that whatever I described in my previous comment was messed up. It is, very :-|Fezzan
It is NSInvocation. You just need to make sure all the classes and selectors are marked with @objc.Chassidychassin
@Tufts I would not say that, why poor architecture?Laryngology
@3000 Selectors and invocations are not type-safe.Tufts
@Sulthan: that's ok, but you can add the required safety by casting the result to your desired type (or you meant something different?)Laryngology
@3000 You can't. The problem is that the name of the method is a string, therefore the compiler cannot check that there even is such a method. This is partially solved for selectors with the #selector syntax but there is no such syntax for NSInvocation. Closures are just safer. There are some other aspects related to memory management since the name of the method in Obj-C affects that. In summary, this is the old way of doing things, we have better and safer alternatives now.Tufts
@Sulthan: ok, thank you for your kind replyLaryngology
C
10

Swift 3.1

NSInvocation can be used dynamically, but only as a fun exercise, definitely not for serious applications. There are better alternatives.

import Foundation

class Test: NSObject {
    @objc var name: String? {
        didSet {
            NSLog("didSetCalled")
        }
    }

    func invocationTest() {
        // This is the selector we want our Invocation to send
        let namePropertySetterSelector = #selector(setter:name)
        
        // Look up a bunch of methods/impls on NSInvocation
        let nsInvocationClass: AnyClass = NSClassFromString("NSInvocation")!
        
        // Look up the "invocationWithMethodSignature:" method
        let nsInvocationInitializer = unsafeBitCast(
            method_getImplementation(
                class_getClassMethod(nsInvocationClass, NSSelectorFromString("invocationWithMethodSignature:"))!
            ),
            to: (@convention(c) (AnyClass?, Selector, Any?) -> Any).self
        )
        
        // Look up the "setSelector:" method
        let nsInvocationSetSelector = unsafeBitCast(
            class_getMethodImplementation(nsInvocationClass, NSSelectorFromString("setSelector:")),
            to:(@convention(c) (Any, Selector, Selector) -> Void).self
        )
        
        // Look up the "setArgument:atIndex:" method
        let nsInvocationSetArgAtIndex = unsafeBitCast(
            class_getMethodImplementation(nsInvocationClass, NSSelectorFromString("setArgument:atIndex:")),
            to:(@convention(c)(Any, Selector, OpaquePointer, NSInteger) -> Void).self
        )
        
        // Get the method signiture for our the setter method for our "name" property.
        let methodSignatureForSelector = NSSelectorFromString("methodSignatureForSelector:")
        let getMethodSigniatureForSelector = unsafeBitCast(
            method(for: methodSignatureForSelector)!,
            to: (@convention(c) (Any?, Selector, Selector) -> Any).self
        )
        
        // ObjC:
        // 1. NSMethodSignature *mySignature = [self methodSignatureForSelector: @selector(setName:)];
        // 2. NSInvocation *myInvocation = [NSInvocation invocationWithMethodSignature: mySignature];
        // 3. [myInvocation setSelector: @selector(setName:)];
        // 4. [myInvocation setArgument: @"new name", atIndex: 2];
        // 5. [myInvocation invokeWithTarget: self];
        
        // 1.
        let namyPropertyMethodSigniature = getMethodSigniatureForSelector(self, methodSignatureForSelector, namePropertySetterSelector)

        // 2.
        let invocation = nsInvocationInitializer(
            nsInvocationClass,
            NSSelectorFromString("invocationWithMethodSignature:"),
            namyPropertyMethodSigniature
        ) as! NSObject // Really it's an NSInvocation, but that can't be expressed in Swift.
        
        // 3.
        nsInvocationSetSelector(
            invocation,
            NSSelectorFromString("setSelector:"),
            namePropertySetterSelector
        )
        
        var localName = "New name" as NSString
        
        // 4.
        withUnsafePointer(to: &localName) { stringPointer in
            nsInvocationSetArgAtIndex(
                invocation,
                NSSelectorFromString("setArgument:atIndex:"),
                OpaquePointer(stringPointer),
                2
            )
        }
        
        // 5.
        invocation.perform(NSSelectorFromString("invokeWithTarget:"), with: self)
    }
}

let object = Test()
object.invocationTest()
Contorted answered 29/4, 2017 at 19:16 Comment(0)
B
5

I'm afraid there is no way to do this in Swift.

However, you may have an Objective-C class to manage your dynamic invocations. You can use NSInvocation there.

Baguette answered 10/11, 2016 at 18:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.