Alex Curran
Alex Curran

Reputation: 8828

Class casting dynamically in swift

I am trying to dyanmically cast to a class in Swift. Is this possible? Here is the code I am trying to use:

let stringClass: AnyClass = NSString.self
let anyObject: AnyObject = "foo"
let string = anyObject as! stringClass

The code fails to compile at the cast. Is this possible and if so, why is the right syntax?

Real use case

Here is the real issue. I am attempting to refactor this code:

switch (value) {
    case "valueOne":
        viewController = storyboard.instantiateViewController(withIdentifier: "foo") as! FirstViewController
    case "valueTwo":
        viewController = storyboard.instantiateViewController(withIdentifier: "bar") as! SecondViewController
    default:
        return nil
}

into:

let controllersDictionary: [String: (String, UIViewController.Type)] = [
    "valueOne" : ("bar", FirstViewController.self),
    "valueTwo" : ("foo", SecondViewController.self)
]
let tuple = controllersDictionary[value]!
let identifier = tuple.0
let cast = tuple.1
let viewController = storyboard.instantiateViewController(withIdentifier: identifier) as! cast

Upvotes: 14

Views: 7953

Answers (5)

Oliver Pearmain
Oliver Pearmain

Reputation: 20590

Its possible so long as you can provide "a hint" to the compiler about the type of... T. So in the example below one must use : String?.

func cast<T>(_ value: Any) -> T? {
    return value as? T
}

let inputValue: Any = "this is a test"
let casted: String? = cast(inputValue) 
print(casted) // Optional("this is a test")
print(type(of: casted)) // Optional<String>

Why Swift doesn't just allow us to let casted = cast<String>(inputValue) I'll never know.


One annoying scenerio is when your func has no return value. Then its not always straightford to provide the necessary "hint". Lets look at this example...

func asyncCast<T>(_ value: Any, completion: (T?) -> Void) {
    completion(value as? T)
}

The following client code DOES NOT COMPILE. It gives a "Generic parameter 'T' could not be inferred" error.

let inputValue: Any = "this is a test"
asyncCast(inputValue) { casted in
    print(casted) 
    print(type(of: casted))
}

But you can solve this by providing a "hint" to compiler as follows:

asyncCast(inputValue) { (casted: String?) in
    print(casted) // Optional("this is a test")
    print(type(of: casted)) // Optional<String>
}

Upvotes: 0

matt
matt

Reputation: 534950

I'm not seeing what the cast at this point is for. You can write:

let controllersDictionary: [String: String] = [
    "valueOne" : "bar",
    "valueTwo" : "foo"
]
let identifier = controllersDictionary[value]!
let viewController = storyboard.instantiateViewController(withIdentifier: identifier)

The cast does nothing for you in the code that you have shown. viewController is typed as UIViewController, but it is the correct underlying view controller subclass thanks to polymorphism; whatever the class is in the storyboard, that's the class of this instance.

The only time you need to cast down is when you have to message an instance with a message belonging only to the subclass, and you have not shown any such need at this point in your code.

Upvotes: 3

GetSwifty
GetSwifty

Reputation: 7746

While there are/will be ways to make this kind of thing work, the Swifty solution (IMO) is to have your desired classes adhere to a protocol that defines the shared behavior you're trying to use, or simply use a super class they have in common

This allows the dynamism requried (in most cases at least) while still allowing the compile-time checks that prevent run time errors.

For your example,

protocol Stringable {
    func toString() -> String
}

extension String: Stringable {
    func toString() -> String {
        return self
    }
}

let thing = "foo"
let anything: Any = thing
let test: String? = (anything as? Stringable)?.toString()

Note that this requires "Any" rather than "AnyObject" since you need to cast to a protocol

Since you mentioned ViewControllers, I thought this might help:

static func createViewController<T: UIViewController>(storyboard: String, scene: String) -> T? {
    return  UIStoryboard(name: storyboard, bundle: nil).instantiateViewControllerWithIdentifier(scene) as? T
}

Upvotes: 1

Alexander
Alexander

Reputation: 63167

I'm not sure exactly what you're trying to achieve, but here's a working version of your example:

func cast<T>(value: Any, to type: T) -> T? {
    return castedValue as? T
}

let inputValue: Any = "this is a test"
let inputType = String.self()
let casted = cast(value: inputValue, to: inputType)

print(casted)

Upvotes: 7

Vatsal
Vatsal

Reputation: 18171

The x as! Y.Type syntax only works when Y is explicitly stated.

So, the compiler wants x as! NSString. The compiler crash is a bug, I suggest that you file a report.

And just think about it for a second, how would that even help you? stringClass is of type AnyClass, and you're force casting an AnyObject which already conforms to AnyClass. You should cast when you have a static type in mind, because casting doesn't really make any sense otherwise.

If you want to check, however, whether your object's class is a subclass of a particular type, you'd use:

x is Y.Type

Upvotes: 0

Related Questions