Search code examples
swiftprotocolsnsviewcontrollerequatable

Swift: Protocol Constraint on iVar + Equatable


Context

Consider this protocol and class in Swift 5.9:

protocol SpinnerHosting: AnyObject
{
    var spinnerItem: SpinnerItem { get }
}
final class MyViewController: NSViewController
{
    var activeChild: (NSViewController & SpinnerHosting)? = nil

    
    func pushChild(_ incomingChild: (NSViewController & SpinnerHosting))
    {
        // #1
        if incomingChild != activeChild {
           ...
        }

        // #2
        if incomingChild != activeChild! {
           ...
        }
    }
}

Problem

At point #1, Swift throws this error:

Type 'any NSViewController & SpinnerHosting' cannot conform to 'Equatable'

At point #2 (ignore the unsafe unwrapping) it throws this one:

Binary operator '!=' cannot be applied to two 'any NSViewController & SpinnerHosting' operands 

Question:

I understand why Protocols aren't Equatable. But I do NOT understand why the compiler thinks this isn't. Here, SpinnerHosting is constrained to AnyObject, which means reference types and therefore pointer-equality. But the same errors persist if I constrain the Protocol to NSViewController itself, which definitely surprises me because NSViewController is equatable.

I'm just trying to say: "Whatever NSViewController is going to occupy activeChild must have a spinnerItem property." (I realize I can do it with a subclass; that isn't my question.)

I've just seen this pattern: var foo: ([Class] & [Protocol]) in several Apple source examples and I don't see a good reason why this can't be Equatable.


Solution

  • activeChild should be Equatable

    You are right, NSViewController inherits from NSObject which conforms to Equatable. So, every subclass of NSViewController is still Equatable.

    As a consequence a variable of type NSViewController, regardless from additional conformance requirements, is still Equatable.

    ✅ Infact, this code compiles just fine.

    protocol SpinnerHosting: AnyObject { }
    class MyViewController: NSViewController, SpinnerHosting { }
    var a: (NSViewController & SpinnerHosting) = MyViewController()
    var b: (NSViewController & SpinnerHosting) = a
    print(a == b)
    

    ❌ The problem arises when we make a and b optional.

    protocol SpinnerHosting: AnyObject { }
    class MyViewController: NSViewController, SpinnerHosting { }
    var a: (NSViewController & SpinnerHosting)? = MyViewController()
    var b: (NSViewController & SpinnerHosting)? = a
    print(a == b) // Type 'any NSViewController & SpinnerHosting' cannot conform to 'Equatable'
    

    Now we get your error 👆

    Wait, but Swift Conditional Conformance allows us to compare optional!

    The most noticeable benefit of conditional conformance is the ability for types that store other types, like Array or Optional, to conform to the Equatable protocol.

    https://www.swift.org/blog/conditional-conformance/

    Right, you can equate 2 optionals if they hold 2 generic elements that can be equated

    ✅ 👇

    protocol SpinnerHosting: AnyObject { }
    class MyViewController: NSViewController, SpinnerHosting { }
    var a: NSViewController? = MyViewController()
    var b: NSViewController? = a
    print(a == b)
    

    However, it seems the Swift compiler is unable to infer Equatable conformance when Existential Types and Conditional Conformance are involved.

    ✅ You can help the Swift compiler adding an explicit casting

    protocol SpinnerHosting: AnyObject { }
    class MyViewController: NSViewController, SpinnerHosting { }
    var a: (NSViewController & SpinnerHosting)? = MyViewController()
    var b: (NSViewController & SpinnerHosting)? = a
    print(a as NSViewController? == b as NSViewController?)
    

    Now it works.

    enter image description here

    Here's another example

    We can do a test replacing Optional with Array (which similarly benefits of Conditional Conformance).

    ✅ This compiles.

    let list: [NSViewController] = []
    list == list
    

    ❌ But as we add existential types to the party, the Conditional Conformance breaks

    let list: [NSViewController & Codable] = []
    list == list // Type 'any NSViewController & Codable' (aka 'any NSViewController & Decodable & Encodable') cannot conform to 'Equatable'
    

    Hope it helps.