Because Equatable
has Self
requirements, it should not be implemented directly on a protocol. Otherwise, the protocol will be unusable as a type.
To implement Equatable
at the protocol level yet be able to use the protocol as a type, you can use type erasure.
To demonstrate, I have modified the code given in your playground to build a type eraser.
For a detailed explanation of the approach I have used, check out this post on my blog:
https://khawerkhaliq.com/blog/swift-protocols-equatable-part-two/
Here is the modified code from your playground:
protocol CustomProtocol {
var title: String { get }
var subtitle: String { get }
func isEqualTo(_ other: CustomProtocol) -> Bool
func asEquatable() -> AnyEquatableCustomProtocol
}
extension CustomProtocol where Self: Equatable {
func isEqualTo(_ other: CustomProtocol) -> Bool {
guard let o = other as? Self else { return false }
return self == o
}
func asEquatable() -> AnyEquatableCustomProtocol {
return AnyEquatableCustomProtocol(self)
}
}
struct A: CustomProtocol, Equatable {
var title: String
var subtitle: String
static func ==(lhs: A, rhs: A) -> Bool {
return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
}
}
struct B: CustomProtocol, Equatable {
var title: String
var subtitle: String
static func ==(lhs: B, rhs: B) -> Bool {
return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle
}
}
struct AnyEquatableCustomProtocol: CustomProtocol, Equatable {
var title: String { return value.title }
var subtitle: String { return value.subtitle }
init(_ value: CustomProtocol) { self.value = value }
private let value: CustomProtocol
static func ==(lhs: AnyEquatableCustomProtocol, rhs: AnyEquatableCustomProtocol) -> Bool {
return lhs.value.isEqualTo(rhs.value)
}
}
// instances typed as the protocol
let a: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let b: CustomProtocol = B(title: "First title", subtitle: "First subtitle")
let equalA: CustomProtocol = A(title: "First title", subtitle: "First subtitle")
let unequalA: CustomProtocol = A(title: "Second title", subtitle: "Second subtitle")
// equality tests
print(a.asEquatable() == b.asEquatable()) // prints false
print(a.asEquatable() == equalA.asEquatable()) // prints true
print(a.asEquatable() == unequalA.asEquatable()) // prints false
The point to note is that with this approach the actual ==
comparisons are delegated to the underlying concrete types, yet we deal only with protocol types to maintain abstraction.
Here I have used the type erased instances only for one comparison. However, since the type eraser conforms to CustomProtocol
these instances can be saved and used in any place where a protocol type is expected. Because they conform to Equatable
, they can also be used in any place where Equatable
conformance is required.
Just for context, this post explains why it is not advisable to try to implement Equatable
conformance or even ==
functions directly on protocols:
https://khawerkhaliq.com/blog/swift-protocols-equatable-part-one/
Hence the type erasure.
Hope this helps.