Black Friday 2024 deal: 30% off our Swift and SwiftUI books! Learn more ...Black Friday 2024 deal:30% off our Swift and SwiftUI books >>

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 X comment.

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

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 X 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
}

As someone who has worked extensively with Swift, I've gathered many such insights over the years. I'm excited to share these in my new book Swift Gems. This book is packed with advanced tips and techniques to help intermediate and advanced Swift developers enhance their coding skills. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice to elevate your Swift development. Grab your copy of Swift Gems and let's explore the advanced techniques together.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Black Friday 2024 offer: 30% off!$35$25

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms
Black Friday 2024 offer: 30% off!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35$25