How to invoke a class method using performSelector() on AnyClass in Swift?
Asked Answered
R

2

9

In ObjC you could simply invoke a class method using the class method from NSObject.

[Machine performSelector:@selector(calculate:) withObject:num];

But how do you do this in Swift 2.2?

@objc(Machine) // put it here, so you can simply copy/paste into Playground
class Machine: NSObject {
    static func calculate(param: NSNumber) -> String {
        if param.integerValue > 5 {
            return "42"
        }
        return "42" // there is only 1 answer to all the questions :D
    }
}

if let aClass = NSClassFromString("Machine") {
    let sel = #selector(Machine.calculate(_:))
    let num = NSNumber(integer: 1337)
    let answer = aClass.performSelector(sel, withObject: num) // compiler error
    // let answer = aClass.calculate(num)                     // <-- this works
    print(answer)
}

With this code I'm getting the following compiler error:

error: cannot invoke 'performSelector' with an argument list of type '(Selector, withObject: NSNumber)'

What am I missing here?

Riviera answered 24/3, 2016 at 19:7 Comment(2)
It is definitely not pointless, what if you want to use the class from an external source? E.G. reading the class name from a string in a plist, are you going to write an if condition based on every possible object?Thermolysis
Related: Access Private UIKit Function Without Using Bridging HeaderVarela
V
17

AnyClass does not conform to NSObjectProtocol out of the box. I had to cast aClass as NSObjectProtocol to use performSelector (performSelector:withObject: is bridged to Swift as a method on NSObjectProtocol):

Swift 3:

if let aClass = NSClassFromString("Machine") {
    let sel = #selector(Machine.calculate(param:))
    let num = NSNumber(value: 1337)

    if let myClass = aClass as? NSObjectProtocol {
        if myClass.responds(to: sel) {
            let answer = myClass.perform(sel, with: num).takeRetainedValue() // this returns AnyObject, you may want to downcast to your desired type
            print(answer) // "42\n"
        }
    }
}

Swift 2.x:

(aClass as! NSObjectProtocol).performSelector(sel, withObject: num) // Unmanaged<AnyObject>(_value: 42) 

A little bit safer:

if let aClass = NSClassFromString("Machine") {
    let sel = #selector(Machine.calculate(_:))
    let num = NSNumber(integer: 1337)

    if let myClass = aClass as? NSObjectProtocol {
        if myClass.respondsToSelector(sel) {
            let answer = myClass.performSelector(sel, withObject: num).takeUnretainedValue()
            print(answer) // "42\n"
        }
    }
}

performSelector returns an Unmanaged object, that's why takeUnretainedValue() (or optionally takeRetainedValue() if you want to transfer memory ownership) are required.

Varela answered 24/3, 2016 at 20:7 Comment(15)
That is funny, because casting it as? AnyObject worked for meThermolysis
@Thermolysis Casting to AnyObject will work because AnyClass is really a typealias for the metatype AnyObject.Type. You probably got a warning that the cast aways succeeds because they're the same type. Probably not a good idea to cast a class to an object though.Varela
@Thermolysis You can only use performSelector on Objective-C types, not pure Swift types. Try it. That's why I check for NSObjectProtocol conformance.Varela
I have no warning on my screen, and wouldn't NSClassFromString automatically fail for swift types?Thermolysis
@Thermolysis Hmm, good point about NSClassFromString. But I still see a warning if I try to cast the class as AnyObject: if let myClass = aClass as? AnyObject { ... } I'm using Xcode 7.3.Varela
yes, weird, use my code and you won't see is, maybe it is because it is AnyClass? ?Thermolysis
ahh, I need to upgrade to 2.2 again, seems like my install went bad, I will get back to you on the warningThermolysis
thanks! I knew I was missing some conformance but didn't know which protocol and where to look it up ;)Riviera
Finally got swift 2.2 working again, anyway, yes, still not warning, Also, since we know that NSClass is going to return a member of NSClass when valid, I do not think the second variable is needed, you can just check if aClass is a member of NSObjectProtocol, and see if it responds to the selector thereThermolysis
@Thermolysis yes, this code can probably be optimized further like if let aClass = NSClassFromString("Machine") as? NSObjectProtocol { ... } but I left the answer expanded so users can see the flow of casting from AnyClass to NSObjectProtocol. I think it's easier to read like this and can always be optimized further as an exercise to the reader.Varela
@Riviera Yes forgot that earlier, so I wanted to make my code a little bit safer in case another user finds it and decides to use it.Varela
"You can only use performSelector on Objective-C types, not pure Swift types." But actually, at runtime, pure Swift types do support .respondsToSelector(), .performSelector(), etc. So casting to AnyObject (to overcome lack of knowledge of such support at compile-time) and using them does work, without needing for the type to conform to NSObjectProtocol.Roast
@Roast Ah you're right! I actually just asked a question about this here. Even though pure Swift classes are represented as SwiftObject to the Objective-C runtime, you need to explicitly declare your object as AnyObject or NSObjectProtocol to fire Objective-C selectors on it.Varela
Oddly, Swift 4 will emit a warning trying to cast AnyClass to NSObjectProtocol, which annoys me. At runtime it still works, but trying to keep clean project of warnings. Does anyone have an idea?Sorb
@Sorb use if let myClass = aClass as AnyObject as? NSObjectProtocol {Tore
T
-1

If you want to perform calculate on machine, just do:

Machine.calculate(NSNumber(integer: 1337))

If you need to call perform selector, then do:

if let aClass = NSClassFromString("Machine") as? AnyObject
{
    let sel = #selector(Machine.calculate(_:))
    let num = NSNumber(integer: 1337)
    let answer = aClass.performSelector(sel, withObject: num)
    print(answer)
}
Thermolysis answered 24/3, 2016 at 19:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.