Swift Utilities — Equatable for Complex Enums

Vyacheslav Ansimov
2 min readNov 19, 2023

--

Over the years as an iOS developer, I’ve accumulated a variety of tools and helpful items that make the development process easier. In this article, I want to share one of those tools. This will not be a long article. I’ll show how to use this utility and demonstrate it in action. I hope the article will be useful for you.

I often encountered situations where it was necessary to conform an enum with nested types to the Equatable protocol and had to implement its functions static

func ==(lhs: T, rhs: T) -> Bool

To simplify life and avoid repeatedly writing complex static func ==(lys: T, res: T) -> Bool, one can conform the enum to the protocol

/// ```swift
/// enum SomeState: ComplexEquatable {
/// case text(SomeText)
/// }
///
/// class SomeText {
/// var text: String?
///
/// init(text: String?) {
/// self.text = text
/// }
/// }
///
/// let objectOne = SomeState.text(.init(text: nil))
/// let objectTwo = SomeState.text(.init(text: "Hello, World!"))
/// objectOne == objectTwo // false
///
/// let objectOne = SomeState.text(.init(text: "Hello, World!"))
/// let objectTwo = SomeState.text(.init(text: "Hello, World!"))
/// objectOne == objectTwo // true
///
/// let objectOne = SomeState.text(.init(text: "Hello"))
/// let objectTwo = SomeState.text(.init(text: "Hello, World!"))
/// objectOne == objectTwo // false
/// ```
public protocol ComplexEquatable: Hashable, Equatable {}

extension ComplexEquatable {

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.hashValue == rhs.hashValue
}

func hash(into hasher: inout Hasher) {
hasher.combine(getObjectInfo(of: self))
}

private func getObjectInfo(of instance: Any) -> String {
let mirror = Mirror(reflecting: instance)
var result = ""

for (index, child) in mirror.children.enumerated() {
if let propertyName = child.label {
result += "\(index == 0 ? "" : ",")\(propertyName):\(child.value)"
let childString = getObjectInfo(of: child.value)
if !childString.isEmpty {
result += "{\(childString)}"
}
}
}
if result == "" {
return String(describing: instance)
}
return result
}
}

The core of ComplexEquatable is the getObjectInfo method, which uses Mirror for object reflection. This method allows for examining the structure of an object and generating a unique hash value based on all its properties. This is key to ensuring that two objects, identical in structure and content, are considered the same.

Example of Use

Let's consider a few examples for enums.

enum SomeState: ComplexEquatable { 
case text(SomeText)
}

class SomeText {
var text: String?

init(text: String?) {
self.text = text
}
}

When we compare two objects of SomeState, one containing nil and the other containing the string “Hello, World!” respectively, the result will be false, as their contents are different.

let objectOne = SomeState.text(.init(text: nil))
let objectTwo = SomeState.text(.init(text: "Hello, World!"))
objectOne == objectTwo // false

However, if we compare two objects both containing the same string “Hello, World!”, the result will be true. This demonstrates the flexibility of ComplexEquatable in various comparison scenarios.

let objectOne = SomeState.text(.init(text: "Hello, World!"))
let objectTwo = SomeState.text(.init(text: "Hello, World!"))
objectOne == objectTwo // true

More stories

--

--