iosswiftswift3selectornsinvocation

Is there any alternative for NSInvocation in Swift?


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

Solution

  • 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()