All articles
iOS9 min read

Swift Protocols That Look the Same But Do Completely Different Things

Master the three essential protocols that let you compare, sort, and store custom types — with automatic synthesis that’ll make you wonder…

K
Karan Pal
Author

Swift Essentials

Image Generated by AI
Image Generated by AI

🤔 The Confusion: They All Sound the Same

Okay so when I first saw Equatable, Comparable, and Hashable in Swift docs, my brain went “aren’t these basically the same thing?”

I mean look at the names. They all end in “-able.” They all seem to be about… comparing stuff? Checking if things are the same? Something like that?

Turns out they’re completely different. And using the wrong one (or not using one when you should) leads to some really confusing compiler errors.

Here’s what happened to me early on. I made a simple struct:

struct GamePlayer {
    let playerID: String
    let username: String
    let score: Int
}

Seemed fine. Then I tried to compare two players:

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player2 = GamePlayer(playerID: "xyz789", username: "NoobSlayer", score: 1200)

if player1 == player2 {  // ❌ Binary operator '==' cannot be applied
    print("Same player")
}

Compiler error. “But why?” I thought. “They’re both GamePlayer instances, just compare them!”

Then I tried putting players in a Set:

var playerSet: Set<GamePlayer> = []  // ❌ Type 'GamePlayer' does not conform to protocol 'Hashable'

Another error. Now it wants Hashable?

Then I tried sorting an array of players:

let players = [player1, player2]
let sorted = players.sorted()  // ❌ Referencing instance method 'sorted(by:)' requires that 'GamePlayer' conform to 'Comparable'

ANOTHER error! This time Comparable!

So which protocol do I need? All three? Just one? What’s the difference?

Yeah, that confusion is super common. Let’s clear it up.

📺 I’m putting together a video breaking down these protocols with real examples. Check it out on Swift Pal when it drops: _https://youtube.com/@swift-pal_

**Swift Pal** _👨‍💻 Welcome to Swift Pal - Your iOS Dev Companion! Whether you're just starting your journey with Swift and SwiftUI…_youtube.comhttps://youtube.com/@swift-pal

⚖️ Equatable: Can Two Things Be Equal?

Equatable is the simplest one. It answers one question: “Are these two things equal?”

That’s it. When you make a type Equatable, you’re saying “here’s how to check if two instances are the same.”

Basic implementation:

struct GamePlayer: Equatable {
    let playerID: String
    let username: String
    let score: Int
}

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player2 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player3 = GamePlayer(playerID: "xyz789", username: "NoobSlayer", score: 1200)

print(player1 == player2)  // true - all properties match
print(player1 == player3)  // false - different values

Notice I just added : Equatable and it worked? That's because Swift automatically synthesizes the implementation for you. It checks if ALL stored properties are equal.

But what if you want custom equality logic?

Maybe you only care about the playerID:

struct GamePlayer: Equatable {
    let playerID: String
    let username: String
    let score: Int
    
    static func == (lhs: GamePlayer, rhs: GamePlayer) -> Bool {
        return lhs.playerID == rhs.playerID
    }
}

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player2 = GamePlayer(playerID: "abc123", username: "DifferentName", score: 2000)

print(player1 == player2)  // true! Only playerID matters

See that static func ==? That's what you implement when you need custom equality. The parameters are typically called lhs and rhs (left-hand side, right-hand side).

When you get automatic synthesis:

Swift gives you Equatable for free when:

struct Point: Equatable {
    let x: Int
    let y: Int
}
// Works automatically - both Int properties are Equatable

struct Rectangle: Equatable {
    let topLeft: Point
    let bottomRight: Point
}
// Also works - Point is Equatable, so Rectangle gets it free

Classes need a bit more work:

class Vehicle: Equatable {
    let vehicleID: String
    let manufacturer: String
    
    init(vehicleID: String, manufacturer: String) {
        self.vehicleID = vehicleID
        self.manufacturer = manufacturer
    }
    
    static func == (lhs: Vehicle, rhs: Vehicle) -> Bool {
        return lhs.vehicleID == rhs.vehicleID &&
               lhs.manufacturer == rhs.manufacturer
    }
}

Classes don’t get automatic synthesis. You gotta write the == function yourself.

What you get with Equatable:

Once your type is Equatable, you can:

let allPlayers = [player1, player2, player3]

if allPlayers.contains(player1) {
    print("Found the player")
}

// Find first match
if let found = allPlayers.first(where: { $0 == player2 }) {
    print("Located: \(found.username)")
}

That’s Equatable. Just equality checking. Nothing fancy.

📊 Comparable: Which One is Bigger?

Comparable builds on Equatable. It’s for ordering things — figuring out which comes first, which is greater, which is smaller.

Think sorting. Ranking. Ordering by priority.

Here’s the key: Comparable requires Equatable. You can’t have Comparable without Equatable. Makes sense — to know if A < B, you also need to know if A == B.

struct GamePlayer: Comparable {
    let playerID: String
    let username: String
    let score: Int
    
    // Comparable requires you implement <
    static func < (lhs: GamePlayer, rhs: GamePlayer) -> Bool {
        return lhs.score < rhs.score
    }
}

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player2 = GamePlayer(playerID: "xyz789", username: "NoobSlayer", score: 1200)
let player3 = GamePlayer(playerID: "def456", username: "EliteGamer", score: 2000)

print(player1 < player2)  // false (1500 > 1200)
print(player2 < player3)  // true (1200 < 2000)

// Now you can sort!
let players = [player1, player2, player3]
let rankedPlayers = players.sorted()
// Sorted by score: [player2 (1200), player1 (1500), player3 (2000)]

for (index, player) in rankedPlayers.enumerated() {
    print("\(index + 1). \(player.username) - \(player.score) points")
}

You only have to implement <. Swift figures out >, <=, and >= for you based on that.

Wait, but I also need Equatable?

Actually, when you conform to Comparable, you need to also specify Equatable. But Swift helps:

struct GamePlayer: Comparable {
    let playerID: String
    let username: String
    let score: Int
    
    // For Comparable
    static func < (lhs: GamePlayer, rhs: GamePlayer) -> Bool {
        return lhs.score < rhs.score
    }
    
    // For Equatable (required by Comparable)
    static func == (lhs: GamePlayer, rhs: GamePlayer) -> Bool {
        return lhs.playerID == rhs.playerID
    }
}

Though honestly, a lot of times you can just let Swift synthesize the == part if it makes sense to compare all properties.

More complex sorting:

What if you want to sort by score first, then by username if scores are tied?

struct GamePlayer: Comparable {
    let playerID: String
    let username: String
    let score: Int
    
    static func < (lhs: GamePlayer, rhs: GamePlayer) -> Bool {
        if lhs.score != rhs.score {
            return lhs.score < rhs.score
        }
        return lhs.username < rhs.username
    }
}

Now players with the same score get sorted alphabetically by username.

What you get with Comparable:

let players = [player1, player2, player3]

let topPlayer = players.max()  // Returns player with highest score
let bottomPlayer = players.min()  // Returns player with lowest score

// Check if in range
let midTierPlayer = player1
if midTierPlayer > player2 && midTierPlayer < player3 {
    print("Mid-tier player")
}

Important: Comparable is specifically for types that have a natural ordering. Not everything should be Comparable. Like, is one image “less than” another? Doesn’t really make sense unless you define it (by file size? dimensions? creation date?).

🔑 Hashable: The Dictionary/Set Enabler

Alright, Hashable is the weird one. It’s not about comparing values directly — it’s about generating a hash value.

“What’s a hash value?” Basically a number that represents your instance. Used internally by Sets and Dictionaries for fast lookups.

Here’s why it matters:

Sets and Dictionaries need to quickly check “is this item already here?” Instead of comparing your object to every single item in the collection (slow), they use hash values for instant lookups.

struct GamePlayer: Hashable {
    let playerID: String
    let username: String
    let score: Int
}

// Now this works!
var uniquePlayers: Set<GamePlayer> = []

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player2 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player3 = GamePlayer(playerID: "xyz789", username: "NoobSlayer", score: 1200)

uniquePlayers.insert(player1)
uniquePlayers.insert(player2)  // Won't be added - same as player1
uniquePlayers.insert(player3)

print(uniquePlayers.count)  // 2 (player1 and player2 are duplicates)

Hashable requires Equatable.

Why? Because after Swift checks hash values, it still needs to confirm with == that items are actually equal. Hash values can collide (different items with same hash), so the final check uses equality.

The hierarchy:

struct GamePlayer: Hashable {  // Automatically makes it Equatable too
    let playerID: String
    let username: String
    let score: Int
}

Just like Equatable, Swift auto-synthesizes Hashable if all your properties are Hashable.

Custom hashing:

Sometimes you want to hash based on specific properties only:

struct GamePlayer: Hashable {
    let playerID: String
    let username: String
    let score: Int
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(playerID)
        // Only hash playerID, ignore username and score
    }
    
    static func == (lhs: GamePlayer, rhs: GamePlayer) -> Bool {
        return lhs.playerID == rhs.playerID
    }
}

Now two players with the same ID but different scores/usernames are considered identical.

Using as Dictionary keys:

struct GamePlayer: Hashable {
    let playerID: String
    let username: String
    let score: Int
}

var playerStats: [GamePlayer: [String]] = [:]

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)

playerStats[player1] = ["Win", "Win", "Loss", "Win"]

if let stats = playerStats[player1] {
    print("Player stats: \(stats)")
}

Can’t do this without Hashable. Dictionaries require Hashable keys.

What you get with Hashable:

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player2 = GamePlayer(playerID: "xyz789", username: "NoobSlayer", score: 1200)
let player3 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)  // duplicate

let allPlayers = [player1, player2, player3]
let uniquePlayers = Set(allPlayers)  // Automatically removes player3 duplicate

print(uniquePlayers.count)  // 2

🔗 How They Relate to Each Other

This is where it clicks. These protocols have a hierarchy:

Equatable (base)

Hashable (needs Equatable)
    
Equatable (base)

Comparable (needs Equatable)

Equatable is the foundation. It’s just “can you check if two things are equal?”

Hashable builds on Equatable. It adds “can you generate a hash value?” Needs Equatable because after hash comparison, Swift double-checks with ==.

Comparable also builds on Equatable. It adds “can you order these things?” Needs Equatable because ordering implies equality (if A == B, then A is not < B or > B).

Can you have all three?

Absolutely. And it’s actually common:

struct GamePlayer: Hashable, Comparable {
    let playerID: String
    let username: String
    let score: Int
    
    // Comparable requirement
    static func < (lhs: GamePlayer, rhs: GamePlayer) -> Bool {
        return lhs.score < rhs.score
    }
    
    // Equatable (auto-synthesized)
    // Hashable (auto-synthesized)
}

let player1 = GamePlayer(playerID: "abc123", username: "ProGamer", score: 1500)
let player2 = GamePlayer(playerID: "xyz789", username: "NoobSlayer", score: 1200)

// Equatable
print(player1 == player2)  // false

// Comparable
print(player1 > player2)  // true

// Hashable - use in Set
var players: Set<GamePlayer> = [player1, player2]

// Comparable - sort
let sorted = [player1, player2].sorted()

One type, all three protocols. Now it can be compared, sorted, hashed, stored in sets, used as dictionary keys — everything.

When do you need which?

Most of the time, if you’re making a data model, just conform to Hashable and Comparable. Gives you maximum flexibility.

⚡ Automatic Synthesis (The Magic)

Swift does a ton of work for you with these protocols. You don’t always have to write the implementation.

Equatable auto-synthesis works when:

✅ All stored properties are Equatable

✅ It’s a struct or enum

✅ Properties aren’t computed

struct Address: Equatable {  // Just add the protocol
    let street: String
    let city: String
    let zipCode: String
}
// Swift writes the == implementation for you!

Hashable auto-synthesis works when:

✅ All stored properties are Hashable

✅ It’s a struct or enum

✅ Type conforms to Equatable (or will auto-conform)

struct Address: Hashable {  // Swift handles everything
    let street: String
    let city: String
    let zipCode: String
}

Comparable does NOT have auto-synthesis.

You always have to implement < yourself. Swift can't guess what "less than" means for your type.

struct Priority: Comparable {
    let level: Int
    let timestamp: Date
    
    // Have to write this yourself
    static func < (lhs: Priority, rhs: Priority) -> Bool {
        if lhs.level != rhs.level {
            return lhs.level < rhs.level
        }
        return lhs.timestamp < rhs.timestamp
    }
}

When auto-synthesis DOESN’T work:

Classes — You have to implement manually

Non-Equatable properties — If any property can’t be compared, you’re on your own

struct Weird: Equatable {
    let name: String
    let closure: () -> Void  // Closures aren't Equatable!
    
    // Have to write custom ==
    static func == (lhs: Weird, rhs: Weird) -> Bool {
        return lhs.name == rhs.name
        // Can't compare closures, so we ignore them
    }
}

Custom equality logic — When you want something different than “all properties equal”

Pro tip: Start with automatic synthesis. Only write custom implementations when you need different behavior.

🎬 Wrapping This Up

So yeah, these three protocols aren’t the same at all. They just sound similar.

Quick recap:

Equatable = “Are these equal?” Lets you use == and !=. Foundation for the others.

Comparable = “Which is bigger/smaller?” Enables sorting and ordering. Requires Equatable.

Hashable = “Generate a hash value.” Enables Sets and Dictionary keys. Requires Equatable.

The relationship:

Swift auto-synthesizes Equatable and Hashable for structs/enums when possible. You always write Comparable yourself.

For most data models? Just conform to Hashable and Comparable. Gives you maximum flexibility — comparison, sorting, sets, dictionaries, everything.

And remember: start with automatic synthesis. Only write custom implementations when the default behavior doesn’t match what you need.

Next time you see “Type ‘Foo’ does not conform to protocol ‘Hashable’” — you’ll know exactly what that means and how to fix it in 30 seconds.

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading