How to prove to the compiler that class conforms to a protocol?

When I was working on my app I ran into this problem that I don't know if my thinking is wrong or I found a bug in Swift or the compiler.

I was trying to store an array of UIViews all of which are guaranteed to conform to a protocol. But I had to upcast these UIViews in order to put them in the same array. When I tried calling a function on these the Compiler threw an error:

Global function 'callFunc(itemType:)' requires that 'UIView' conform to 'Identifiable'

In the following example, I took out all the unnecessary code in order to isolate the problem. For this reason, the code lost its usefulness in application but the principle still stands.

protocol Identifiable: class {
    static var id: String { get }
}

extension Identifiable {
    static var id: String {
        return String(describing: self)
    }
}

typealias IdentifiableView = UIView & Identifiable

func identify<T>(_ view: T.Type) where T: UIView, T: Identifiable {
    print(view.id)
}

class MyViewA: IdentifiableView { }
class MyViewB: IdentifiableView { }

let views: [IdentifiableView.Type] = [MyViewA.self, MyViewB.self]

// flag-1: This works
identify(MyViewA.self)
identify(MyViewB.self)

for view in views {
    
    // flag-2: This doesn't work
//    identify(view)
    
    // flag-3: This works
    print(view.id)
    
    // flag-4: This works
    if let view = view as? MyViewA.Type {
        identify(view)
    } else if let view = view as? MyViewB.Type {
        identify(view)
    }
}

The problem in this code is at flag-2 and flag-3:

At flag-2 the functions aren't willing on taking the view of type IdentifiableView.Type even though it fulfils all the requirements. I prove this at flag-3 by getting the id property of the type. This is what the function would do.

At flag-4 when I downcast to the most specific type it works again.

My theory as to why this is working the way it is because in the eye of the compiler the type loses its protocol conformance when upcasted. However, this is not the case as proven by flag-3.

I even tried changing the function to the following but even like this, the same error occurs:

func identify<T>(_ view: T.Type) where T: IdentifiableView {
    print(view.id)
}

I also started a small tread before on Reddit.

So is my thinking wrong? is my theory correct? Why is the error thrown? I would much appreciate some feedback.

The existential does not conform to the protocol. This is a known limitation in Swift, and there have been numerous discussions about when and whether it is possible to lift the restriction. Currently, however, protocol existentials do not conform to the protocol they represent.

Here is a minimal example:

protocol P {}
struct S: P {}

func f<T: P>(_ t: T) {}

let s: S = S()
let p: P = S()

f(s)    // valid
f(p)    // error
// Protocol type 'P' cannot conform to 'P' because only concrete types can conform to protocols

• • •

As a workaround, you can change the signature of the function to accept the existential typealias:

func identify(_ view: IdentifiableView.Type) {
  print(view.id)
}
1 Like

The other usual solution to this is to use type erasure: https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/. In short, you create a base class that conforms to your protocol, create a generic subclass that forwards the protocol methods to its wrapped generic objects/type, and then have an array of e.g. AnyIdentifiable rather than Identifiable existentials. It’s fairly cumbersome, but does work in contexts where arrays of existentials wouldn’t work (e.g. if the protocol has an associated type).

If you’re able to use the protocol (or its type) as existentials, however, @Nevin’s solution is much simpler.