Check if two values of type Any are equal

In Swift 5.7 that comes with Xcode 14 we can more easily check if two values of type Any are equal, because we can cast values to any Equatable and also use any Equatable as a parameter type thanks to Unlock existentials for all protocols change.

We can start by extending Equatable protocol with isEqual() method that accepts another any Equatable as an argument.

extension Equatable {
    func isEqual(_ other: any Equatable) -> Bool {
        guard let other = other as? Self else {
            return false
        }
        return self == other
    }
}

Inside isEqual() method we have access to Self, so we can try to cast the other value to the same type as the concrete type of the value this method is called on. If the cast fails, the two values cannot be equal, so we return false. If it succeeds, then we can check the two values for equality, using == function on values of the same concrete type.

If we don't use this method with class subtypes, it will work well. However, it won't always provide consistent results when checking a child and a parent class for equality, as Tikitu de Jager pointed out in his Twitter comment.

Our isEqual() method can be improved by adding the inverse check, and David Peterson suggested a great solution for that, also on Twitter.

extension Equatable {
    func isEqual(_ other: any Equatable) -> Bool {
        guard let other = other as? Self else {
            return other.isExactlyEqual(self)
        }
        return self == other
    }
    
    private func isExactlyEqual(_ other: any Equatable) -> Bool {
        guard let other = other as? Self else {
            return false
        }
        return self == other
    }
}

Note, that there are still some edge cases that are not covered, like checking two sibling classes for equality. And as Becca Royal-Gordon noted in her Twitter comment, this won't treat unbridged and bridged Foundation types as equal, like AnyHashable does. But as long as we are aware of the limitations, we can still use this solution in our projects.

Now we can define a global areEqual() function that will compare two values of type Any. Unfortunately, we can't' extend Any type, so it will have to be a global function.

func areEqual(first: Any, second: Any) -> Bool {
    guard
        let equatableOne = first as? any Equatable,
        let equatableTwo = second as? any Equatable
    else { return false }
    
    return equatableOne.isEqual(equatableTwo)
}

Inside areEqual() we can try to cast both of Any values to any Equatable and if we succeed, we can call our previously defined isEqual() method on one of the values, passing the other values as an argument.

Overloading == operator for two values of type Any seems to be causing a run-time crash because it enters a recursive loop in Swift at the moment, even if I put @_disfavoredOverload on it. When we have different overloads with parameters of concrete types and protocols, Swift can't always disambiguate them. @_disfavoredOverload should usually help with that, but it didn't work in this case.

Another way to overload == operator would be to set generic parameters without constraints, so that Swift doesn't choose this version when it has two concrete types. This will let us compare two Any values as well.

func ==<L, R>(lhs: L, rhs: R) -> Bool {
    guard
        let equatableLhs = lhs as? any Equatable,
        let equatableRhs = rhs as? any Equatable
    else { return false }
    
    return equatableLhs.isEqual(equatableRhs)
}

Now we can use our areEqual() function or == overload, when we need to compare two values of type Any. For example, we might want to check if two dictionaries of NSAttributedString attributes are equal without casting them to NSDictionary.

typealias NSAttributes = [NSAttributedString.Key: Any]

func ==(lhs: NSAttributes, rhs: NSAttributes) -> Bool {
    if lhs.isEmpty && rhs.isEmpty { return true }
    
    guard lhs.count == rhs.count else { return false }
    
    for (key, lhsValue) in lhs {
        guard let rhsValue = rhs[key] else {
            return false
        }
        if !areEqual(first: lhsValue, second: rhsValue) {
            return false
        }
    }
    
    return true
}