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
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
Aswath
  • 1,236
  • 1
  • 14
  • 28
  • What exactly is the problem you're trying to solve? – Alexander Nov 10 '16 at 17:45
  • 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. – Aswath Nov 10 '16 at 18:00
  • 1
    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. – Sulthan Nov 10 '16 at 18:58
  • 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 :-| – Aswath Nov 10 '16 at 19:06
  • It is `NSInvocation`. You just need to make sure all the classes and selectors are marked with `@objc`. – Bryan Chen Nov 10 '16 at 20:24
  • 1
    @Sulthan I would not say that, why poor architecture? –  Aug 03 '17 at 07:55
  • @3000 Selectors and invocations are not type-safe. – Sulthan Aug 03 '17 at 08:05
  • @Sulthan: that's ok, but you can add the required safety by casting the result to your desired type (or you meant something different?) –  Aug 03 '17 at 08:22
  • 1
    @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. – Sulthan Aug 03 '17 at 08:30
  • @Sulthan: ok, thank you for your kind reply –  Aug 03 '17 at 08:32

2 Answers2

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()
Kamil.S
  • 5,205
  • 2
  • 22
  • 51
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.

Marcos Crispino
  • 8,018
  • 5
  • 41
  • 59