8

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?

nhgrif
  • 61,578
  • 25
  • 134
  • 173
Buju
  • 1,546
  • 3
  • 16
  • 27
  • 1
    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? – Knight0fDragon Mar 24 '16 at 19:54
  • Related: [Access Private UIKit Function Without Using Bridging Header](http://stackoverflow.com/q/35183818/2415822) – JAL Mar 24 '16 at 20:16

2 Answers2

16

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.

JAL
  • 41,701
  • 23
  • 172
  • 300
  • That is funny, because casting it as? AnyObject worked for me – Knight0fDragon Mar 24 '16 at 20:56
  • @Knight0fDragon 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. – JAL Mar 24 '16 at 20:59
  • @Knight0fDragon You can only use `performSelector` on Objective-C types, not pure Swift types. Try it. That's why I check for `NSObjectProtocol` conformance. – JAL Mar 24 '16 at 21:03
  • I have no warning on my screen, and wouldn't NSClassFromString automatically fail for swift types? – Knight0fDragon Mar 24 '16 at 21:32
  • @Knight0fDragon 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. – JAL Mar 24 '16 at 21:37
  • yes, weird, use my code and you won't see is, maybe it is because it is AnyClass? ? – Knight0fDragon Mar 24 '16 at 21:40
  • ahh, I need to upgrade to 2.2 again, seems like my install went bad, I will get back to you on the warning – Knight0fDragon Mar 24 '16 at 21:46
  • thanks! I knew I was missing some conformance but didn't know which protocol and where to look it up ;) – Buju Mar 25 '16 at 09:36
  • 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 there – Knight0fDragon Mar 28 '16 at 00:09
  • @Knight0fDragon 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. – JAL Mar 29 '16 at 16:27
  • @Buju 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. – JAL Mar 29 '16 at 16:28
  • 1
    "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`. – newacct Apr 03 '16 at 09:10
  • @newacct Ah you're right! I actually just asked a question about this [here](http://stackoverflow.com/q/36851866/2415822). 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. – JAL Apr 28 '16 at 14:16
  • 1
    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? – Legoless Aug 29 '17 at 09:53
  • 1
    @Legoless use `if let myClass = aClass as AnyObject as? NSObjectProtocol {` – iHTCboy Feb 20 '22 at 03:08
-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)
}
Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44