Failing cast in Swift from Any? to protocol
Asked Answered
I

6

28

FYI: Swift bug raised here: https://bugs.swift.org/browse/SR-3871


I'm having an odd problem where a cast isn't working, but the console shows it as the correct type.

I have a public protocol

public protocol MyProtocol { }

And I implement this in a module, with a public method which return an instance.

internal struct MyStruct: MyProtocol { }

public func make() -> MyProtocol { return MyStruct() }

Then, in my view controller, I trigger a segue with that object as the sender

let myStruct = make()
self.performSegue(withIdentifier: "Bob", sender: myStruct)

All good so far.

The problem is in my prepare(for:sender:) method.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "Bob" {
        if let instance = sender as? MyProtocol {
            print("Yay")
        }
    }
}

However, the cast of instance to MyProtocol always returns nil.

When I run po sender as! MyProtocol in the console, it gives me the error Could not cast value of type '_SwiftValue' (0x1107c4c70) to 'MyProtocol' (0x1107c51c8). However, po sender will output a valid Module.MyStruct instance.

Why doesn't this cast work?

(I've managed to solve it by boxing my protocol in a struct, but I'd like to know why it's not working as is, and if there is a better way to fix it)

Incentive answered 3/2, 2017 at 22:3 Comment(2)
just going out on a limb here, but does changing the internal declaration here internal struct MyStruct: MyProtocol { } to public change anything?Brit
@Brit Nope :(Incentive
M
42

This is pretty weird bug – it looks like it happens when an instance has been bridged to Obj-C by being boxed in a _SwiftValue and is statically typed as Any(?). That instance then cannot be cast to a given protocol that it conforms to.

According to Joe Groff in the comments of the bug report you filed:

This is an instance of the general "runtime dynamic casting doesn't bridge if necessary to bridge to a protocol" bug. Since sender is seen as _SwiftValue object type, and we're trying to get to a protocol it doesn't conform to, we give up without also trying the bridged type.

A more minimal example would be:

protocol P {}
struct S : P {}

let s = S()

let val : Any = s as AnyObject // bridge to Obj-C as a _SwiftValue.

print(val as? P) // nil

Weirdly enough, first casting to AnyObject and then casting to the protocol appears to work:

print(val as AnyObject as! P) // S()

So it appears that statically typing it as AnyObject makes Swift also check the bridged type for protocol conformance, allowing the cast to succeed. The reasoning for this, as explained in another comment by Joe Groff, is:

The runtime has had a number of bugs where it only attempts certain conversions to one level of depth, but not after performing other conversions (so AnyObject -> bridge -> Protocol might work, but Any -> AnyObject -> bridge -> Protocol doesn't). It ought to work, at any rate.

Mcilroy answered 3/2, 2017 at 23:15 Comment(2)
It i I have extended Array using a protocol P but this casting is not working for that casePerpend
This is an excellent write-up of a super weird bug. Thanks for the chained casting tip, I didn't even know that was valid code!Norven
R
4

Still not fixed. My favorite and easiest workaround is by far chain casting:

if let instance = sender as AnyObject as? MyProtocol {

}
Rugose answered 20/4, 2019 at 22:44 Comment(0)
B
3

The problem is that the sender must pass through the Objective-C world, but Objective-C is unaware of this protocol / struct relationship, since both Swift protocols and Swift structs are invisible to it. Instead of a struct, use a class:

protocol MyProtocol {}
class MyClass: MyProtocol { }
func make() -> MyProtocol { return MyClass() }

Now everything works as you expect, because the sender can live and breathe coherently in the Objective-C world.

Borkowski answered 3/2, 2017 at 22:48 Comment(1)
I'm not saying that the behavior you've isolated is right. The struct is being boxed, but you'd think its relationship with the protocol would survive that. You might want to file a bug with bugs.swift.org.Borkowski
R
1

I came across this issue on macOS 10.14. I have an _NSXPCDistantObject coming from Objc for which

guard let obj = remoteObj as? MyProtocol else { return } 

returns

My solution was to define a c function in a separate header like this:

static inline id<MyProtocol> castObject(id object) {
     return object
}

And then use like this:

guard let obj: MyProtocol = castObject(remoteObject) else { return }
Roberto answered 27/1, 2023 at 14:16 Comment(0)
I
0

Here's my solution. I didn't want to just make it into a class (re: this answer) because my protocol is being implemented by multiple libraries and they would all have to remember to do that.

I went for boxing my protocol into a struct.

public struct BoxedMyProtocol: MyProtocol {
    private let boxed: MyProtocol

    // Just forward methods in MyProtocol onto the boxed value
    public func myProtocolMethod(someInput: String) -> String {
        return self.boxed.myProtocolMethod(someInput)
    }
}

Now, I just pass around instances of BoxedMyProtocol.

Incentive answered 7/2, 2017 at 7:58 Comment(0)
V
0

I know this issue was resolved with swift 5.3 but sometimes you have to support older versions of iOS.

You can cast to the protocol if you first cast it to AnyObject.

if let value = (sender as? AnyObject) as? MyProtocol {
   print("Yay")
}
Vineland answered 2/2, 2022 at 15:39 Comment(1)
Same solution as here?Folium

© 2022 - 2024 — McMap. All rights reserved.