I have a protocol, ViewEdgeConvertible, that’s used to convert between directional and fixed view edges using a UITraitCollection:
public protocol ViewEdgeConvertible {
func fixedViewEdge(
using traitCollection: UITraitCollection?
) -> FixedViewEdge
func directionalViewEdge(
using traitCollection: UITraitCollection?
) -> DirectionalViewEdge
}
public enum FixedViewEdge: Equatable {
case top
case left
case right
case bottom
}
public enum DirectionalViewEdge: Equatable {
case top
case leading
case trailing
case bottom
}
Using this, I can convert freely between the two types given a trait collection, which lets me account for right-to-left layouts. But when I try to make an == implementation:
public func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
}
I get this warning: All paths through this function will call itself.
It looks like the compiler sees the == method between the two FixedViewEdge instances and defaults to this method I’ve implemented. Is there a way to write this that takes advantage of the synthesized Equatable conformance on the FixedViewEdge enum?
Jon_Shier
(Jon Shier)
2
Perhaps make lhs and rhs generic types that are constrained to ViewEdgeConvertible and Equatable?
This has the same result (and results in an infinite loop at runtime):
public func == <T: ViewEdgeConvertible>(
lhs: T,
rhs: T
) -> Bool where T: Equatable {
return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
}
This works but is ugly:
public func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
switch (lhs.fixedViewEdge(using: nil), rhs.fixedViewEdge(using: nil)) {
case (.top, .top), (.left, .left), (.right, .right), (.bottom, .bottom):
return true
default:
return false
}
}
Jumhyn
(Frederick Kellison-Linn)
5
FWIW, I am not seeing the error in a playground running in Xcode 12 beta 4.
To reproduce the issue you need to add the implementation that makes the types conform to ViewEdgeConvertible:
extension FixedViewEdge: ViewEdgeConvertible {
public func fixedViewEdge(
using traitCollection: UITraitCollection?
) -> FixedViewEdge {
self
}
public func directionalViewEdge(
using traitCollection: UITraitCollection?
) -> DirectionalViewEdge {
switch (self, traitCollection?.layoutDirection) {
case (.top, _): return .top
case (.left, .rightToLeft): return .trailing
case (.left, _): return .leading
case (.right, .rightToLeft): return .leading
case (.right, _): return .trailing
case (.bottom, _): return .bottom
}
}
}
extension DirectionalViewEdge: ViewEdgeConvertible {
public func fixedViewEdge(
using traitCollection: UITraitCollection?
) -> FixedViewEdge {
switch (self, traitCollection?.layoutDirection) {
case (.top, _): return .top
case (.leading, .rightToLeft): return .right
case (.leading, _): return .left
case (.trailing, .rightToLeft): return .left
case (.trailing, _): return .right
case (.bottom, _): return .bottom
}
}
public func directionalViewEdge(
using traitCollection: UITraitCollection?
) -> DirectionalViewEdge {
self
}
}
Jumhyn
(Frederick Kellison-Linn)
7
Thanks, I'm now able to reproduce! Here's a slightly condensed version of your workaround which also silences the error:
public func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
switch lhs.fixedViewEdge(using: nil) {
case rhs.fixedViewEdge(using: nil):
return true
default:
return false
}
}
ETA: In any case, I would recommend filing a bug on bugs.swift.org
1 Like
Lantua
9
You can also implement FixedViewEdge.== to bump its priority:
public func ==(lhs: FixedViewEdge, rhs: FixedViewEdge) -> Bool {
switch (lhs, rhs) {
case (.top, .top), (.bottom, .bottom), (.left, .left), (.right, .right): return true
default: return false
}
}
You probably also want to do that on DirectionalViewEdge so that directionalViewEdge1 == directionalViewEdge2 use the more performant Equatable implementation. (or maybe it'll get constant-folded anyway).
Personally, I'm not sure it's advisable to implement == over heterogeneous types.
3 Likes
I think you need to move the method into an extension on ViewEdgeConvertible, rather than keep it as a free function:
extension ViewEdgeConvertible {
public static func == (lhs: ViewEdgeConvertible, rhs: ViewEdgeConvertible) -> Bool {
return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
}
}
That should silence the warning for you.
2 Likes
Thank you, this does indeed silence the warning!
Lantua
14
That's very spooky, that a free function == and a static function ViewEdgeConvertible.== have different priorities (one below synthesized == and one above). 
Using the extension does satisfy the warning, but now I can’t seem to use the method. In a test, I have this line:
XCTAssertTrue(edge == edge)
And this fails to compile with the error Binary operator '==' cannot be applied to two 'ViewEdgeConvertible' operands. This is what I have for ==:
extension ViewEdgeConvertible {
public static func == (
lhs: ViewEdgeConvertible,
rhs: ViewEdgeConvertible
) -> Bool {
return lhs.fixedViewEdge(using: nil) == rhs.fixedViewEdge(using: nil)
}
}
Using the workaround from @Jumhyn, I can at least call == from a test. Very odd.