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! {
...
}
}
}
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
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
.
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 👆
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.
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.