Swift and SwiftUI tutorials for Swift Developers

Equatable Protocol in Swift

If you are an iOS Developer looking to take your skills to the next level, you have probably encountered performance issues: stuttering animations, views reloading for no reason, and excessive CPU usage. In the Apple ecosystem, efficiency is key.

In this article, we are going to dive deep into one of the most fundamental and powerful concepts in Swift programming: the Equatable protocol in SwiftUI. We will learn not only what it is, but how to implement it step-by-step in Xcode to optimize your SwiftUI applications across multiple platforms, including iOS, macOS, and watchOS.


What is the Equatable protocol in Swift programming?

In its most basic form, the Equatable protocol in Swift allows two instances of the same type to be compared to know if they are “equal” or “different”.

When you use the equality (==) or inequality (!=) operators, you are using the Equatable protocol under the hood. If you create a custom struct or class in Swift and do not conform to this protocol, the compiler won’t know how to compare two instances of your model and will throw an error.

The anatomy of Equatable

The protocol itself is incredibly simple. It only requires the implementation of a static function:

static func == (lhs: Self, rhs: Self) -> Bool
  • lhs (Left Hand Side): The value on the left side of the == operator.
  • rhs (Right Hand Side): The value on the right side of the == operator.

Automatic Synthesis in Swift

One of the great advantages of modern Swift programming is that, in most cases, you don’t have to write this function manually. If you have a Struct where all its properties already conform to the Equatable protocol (like String, Int, Bool, etc.), Swift automatically synthesizes the implementation for you.

// Swift generates the == function automatically
struct User: Equatable {
    let id: UUID
    let name: String
    let age: Int
}

let user1 = User(id: UUID(), name: "Anna", age: 28)
let user2 = User(id: UUID(), name: "Anna", age: 28)

// This is possible thanks to Equatable
if user1 == user2 {
    print("They are exactly equal")
}

Manual Implementation: Taking Control

Sometimes, automatic synthesis isn’t what you need. As an iOS Developer, you will encounter situations where two objects must be considered “equal” based solely on a unique identifier, even if other properties (like a temporary UI state) have changed.

struct BlogPost: Equatable {
    let id: String
    var title: String
    var reads: Int
    
    // Manual implementation
    static func == (lhs: BlogPost, rhs: BlogPost) -> Bool {
        // We only care that the ID is the same to consider them the same article
        return lhs.id == rhs.id
    }
}

By doing this, you tell Swift exactly under what rules two instances are identical. This becomes crucial when working with reactive user interfaces.


Equatable protocol in SwiftUI: The Key to Performance

To understand why the Equatable protocol in SwiftUI is so vital, we need to understand how SwiftUI draws screens.

SwiftUI is a state-based declarative framework. When a view’s state changes (for example, via @State or @ObservedObject), SwiftUI evaluates the view hierarchy, compares the new view with the old view, and calculates which parts of the screen need to be redrawn. This process is known as diffing.

The problem with unnecessary reloads

Imagine you have a parent view that updates a temporary counter every second, but also contains a heavy child view displaying a user’s profile. By default, when the parent updates, the child view might also be re-evaluated.

The solution: .equatable()

By making the child view conform to Equatable and applying the .equatable() modifier, you are telling SwiftUI: “Hey, before spending resources redrawing this view, compare its current state with the new one using the == function. If it returns true, skip it and don’t redraw it”.


Xcode Tutorial Guide: Multiplatform (iOS, macOS, watchOS)

Next, we are going to create a small project in Xcode that demonstrates how to use this in practice. The code we write will work perfectly on iOS, macOS, and watchOS.

Step 1: Set Up the Model

First, we create our data model. We ensure it is Equatable.

import Foundation

struct Product: Identifiable, Equatable {
    let id: UUID
    let name: String
    var price: Double
    var inStock: Bool
    
    // We opt for Swift's automatic synthesis. 
    // Two products are equal if ALL their properties are equal.
}

Step 2: Create the heavy child view (Equatable View)

Now, in Xcode, we create a view that represents our product cell. This view will simulate having complex rendering (for example, loading heavy images or doing calculations).

import SwiftUI

struct ProductCellView: View, Equatable {
    let product: Product
    
    var body: some View {
        // We simulate a log to see when SwiftUI actually redraws this view
        let _ = print("Redrawing ProductCellView for: \(product.name)")
        
        VStack(alignment: .leading) {
            Text(product.name)
                .font(.headline)
            Text("Price: $\(product.price, specifier: "%.2f")")
                .foregroundColor(.secondary)
            
            if product.inStock {
                Text("In Stock")
                    .font(.caption)
                    .padding(4)
                    .background(Color.green.opacity(0.2))
                    .cornerRadius(4)
            } else {
                Text("Out of Stock")
                    .font(.caption)
                    .padding(4)
                    .background(Color.red.opacity(0.2))
                    .cornerRadius(4)
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(10)
    }
    
    // Explicit manual implementation for SwiftUI
    // If the product is the same, the view is the same and shouldn't be redrawn.
    static func == (lhs: ProductCellView, rhs: ProductCellView) -> Bool {
        return lhs.product == rhs.product
    }
}

Step 3: The parent view and optimization

Now we create the main view. We’ll have a button that changes a state irrelevant to the products (like a color theme or a timer), to demonstrate how we prevent ProductCellView from reloading.

struct CatalogView: View {
    @State private var products: [Product] = [
        Product(id: UUID(), name: "MacBook Pro", price: 2499.0, inStock: true),
        Product(id: UUID(), name: "iPhone 15 Pro", price: 999.0, inStock: true),
        Product(id: UUID(), name: "Apple Watch Ultra", price: 799.0, inStock: false)
    ]
    
    // A state that changes frequently but doesn't affect the products
    @State private var irrelevantCounter: Int = 0
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                
                Text("Click counter: \(irrelevantCounter)")
                    .font(.title)
                
                Button("Update Counter") {
                    irrelevantCounter += 1
                }
                .buttonStyle(.borderedProminent)
                
                Divider()
                
                ForEach(products) { product in
                    // HERE IS THE MAGIC!
                    // We use .equatable() so SwiftUI uses our == method
                    ProductCellView(product: product)
                        .equatable() 
                }
            }
            .padding()
        }
    }
}

What happens when compiling in Xcode?

  1. Without .equatable(): Every time you press “Update Counter”, the irrelevantCounter state changes. SwiftUI re-evaluates CatalogView. You will see in the Xcode console that the message “Redrawing ProductCellView…” is printed several times per click, wasting CPU cycles.
  2. With .equatable(): By pressing the button, the parent changes, but when SwiftUI reaches ProductCellView, it executes the == function. Since the properties of the product haven’t changed, the function returns true. SwiftUI stops the update on that branch of the view tree. The console remains clean. Performance optimized!

Advanced Use Cases and Common Pitfalls

Like any good iOS Developer, you should know that this tool is not a silver bullet. Use it with caution following these rules:

1. Do not optimize prematurely

The SwiftUI diffing engine is already extremely fast and efficient. You do not (and should not) apply .equatable() to every small Text or Image in your application. Use the Equatable protocol in SwiftUI only when you identify a real performance bottleneck, typically in views that:

  • Contain a lot of heavy mathematical logic in their body.
  • Render complex graphics.
  • Are inside a massive ScrollView or a List with thousands of elements that change very frequently.

2. Beware of Reference Types (Classes)

In Swift programming, if your model is a class instead of a struct, the == comparison can be misleading if you compare memory references (using ===) instead of actual values. For SwiftUI, it is highly recommended to use Structs (Value Types) for UI data.

3. @Binding and Equatable

Comparing views that contain @Binding can be problematic. A Binding does not easily conform to Equatable because it represents a bidirectional connection, not just a static value. If you need to make an equatable view that uses bindings, it is often better to extract the underlying value or redesign the data flow to avoid passing complex bindings to views that require high rendering optimization.


Conclusion

Mastering the Equatable protocol in SwiftUI and Swift programming in general is an essential step for any iOS Developer aspiring to create professional-quality applications. Understanding how Xcode compiles these instructions and how SwiftUI decides what to paint on the screen gives you the power to create fluid interfaces, maximizing the battery life of iOS, macOS, and watchOS devices.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Sendable Protocol in Swift

Next Article

NavigationSplitView on macOS with SwiftUI

Related Posts