Introducing Equatable: package that provides macros for generating Equatable conformances for structs for high performance SwiftUi view diffing

Cal Stephens (@cal) wrote an excellent blog post, Understanding and Improving SwiftUI Performance.

The post gives us insight into how SwiftUI’s performance hinges on efficient view diffing—but the default behavior often falls short, especially with closures and complex view models. Manually writing Equatable conformances is tedious and error-prone.

An Equatable macro is introduced to solve this by generating optimized Equatable implementations for your SwiftUI views, ensuring accurate and performant diffing out of the box. It eliminates boilerplate, prevents regressions, and helps you avoid unnecessary re-renders—boosting UI performance while keeping your codebase clean and maintainable.

Ideal for teams scaling SwiftUI in large apps.

We at Ordo One decided to implement the macro and open-source it for the community.

Usage

Apply the @Equatable macro to structs to automatically generate Equatable conformance:

import Equatable
import SwiftUI

@Equatable
struct ProfileView: View {
    var username: String   // Will be compared
    @State private var isLoading = false           // Automatically skipped
    @ObservedObject var viewModel: ProfileViewModel // Automatically skipped
    @EquatableIgnored var cachedValue: String? // This property will be excluded
    @EquatableIgnoredUnsafeClosure var onTap: () -> Void // This closure is safe and will be ignored in comparison (in order for it to be safe we must be sure that this closure does not capture value types on call site)
    let id: UUID // Will be compared first for short-circuiting equality checks
    
    var body: some View {
        VStack {
            Text(username)
            if isLoading {
                ProgressView()
            }
        }
    }
}

The generated extension will implement the == operator with property comparisons ordered for optimal performance (e.g., IDs and simple types first):

extension ProfileView: Equatable {
    nonisolated public static func == (lhs: ProfileView, rhs: ProfileView) -> Bool {
        lhs.id == rhs.id && lhs.username == rhs.username
    }
}

We would love to hear your feedback!

14 Likes

Nice! Looking forward to taking this library for a spin. :clap:

Also: IIUC, it's also necessary to wrap a view in a EquatableView (or the shorthand equatable() modifier) so as to opt out of SwiftUI’s built-in reflection-based diffing.

1 Like

I would suggest renaming this to EquatableView, both for clarity, and also because it becomes the obvious name if you add .equatable() using the macro.

3 Likes

This isn't necessary -- SwiftUI automatically uses an Equatable conformance if present rather than the default reflection-based comparison algorithm.

If a view is POD, then SwiftUI will compare the bits directly rather than using the Equatable conformance. However, POD views are pretty uncommon[1], and if your view is POD then you shouldn't be needing to use @EquatableIgnored in the view anyway.


  1. Any SwiftUI property wrapper like @State makes a view not be POD, in addition to non-POD types like String, Array, Dictionary (all copy-on-write reference types under the hood), or any other reference type. Views without any of these are pretty uncommon and probably don't need to be optimized with @Equatable. ↩︎

5 Likes

We will be using this for non-SwiftUI use cases too, so such renaming would be misleading - it is clearly useful for SwiftUI - but there are other uses too…

Thanks for clarifying. Then I'm struggling to see a use case where one would reach for a EquatableView. This view appears to be a relic of the early SwiftUI days and seems ripe for deprecation...

1 Like

Speaking of non-SwiftUI use cases, the SwiftFormat redundantEquatable rule can automatically adopt your @Equatable macro on classes in your project:

  // Using --equatable-macro @Equatable,MyMacroLib
  import FooLib
+ import MyMacroLib

+ @Equatable
+ class Bar {
- class Bar: Equatable {
      let baaz: Baaz

-     static func ==(lhs: Bar, rhs: Bar) -> Bool {
-         lhs.baaz == rhs.baaz
-     }
  }
1 Like

If that's the case, then I think that while this library is focused only on Equatable, you unfortunately are forced to also support @Hashable. Otherwise it's possible to do something seemingly innocuous that is hiding a problem:

@Equatable
struct User: Hashable {
  let id: Int 
  @EquatableIgnored
  var name = ""
}

In this type:

let userA = User(id: 1, name: "Blob")
let userB = User(id: 1, name: "Blob Jr")
userA == userB  // ✅ true 
userA.hashValue == userB.hashValue  // 🛑 false

This can lead to all types of problems, including runtime crashes, due to the hash invariant not being upheld.

11 Likes

Good catch for the general case, thanks!

Yes itis heplful for us

Very helpful

Thanks again, sorted now with 1.0.4 / feat(patch): generate correct implementation if type conforms to by supersonicbyte · Pull Request #13 · ordo-one/equatable · GitHub

2 Likes

With 1.1.0 release, Equatable macro supports generating the conformance with different isolation levels by using the isolation parameter. The parameter accepts three values: .nonisolated (default), .isolated, and .main (requires Swift 6.2 or later). The chosen isolation level will be applied to the generated conformances for both Equatable and Hashable (if applicable).

Nonisolated (default)

The generated Equatable conformance is nonisolated, meaning it can be called from any context without isolation guarantees.


@Equatable(isolation: .nonisolated) (also omitting the parameter uses this mode)
struct Person {
    let name: String
    let age: Int
}


expands to:

extension Person: Equatable {
  nonisolated public static func == (lhs: Person, rhs: Person) -> Bool {
    lhs.name == rhs.name && lhs.age == rhs.age
  }
}

Isolated

The generated Equatable conformance is isolated, meaning it can only be called from within the actor's context.


@Equatable(isolation: .isolated)
struct Person {
    let name: String
    let age: Int
}


expands to:

extension Person: Equatable {
  public static func == (lhs: Person, rhs: Person) -> Bool {
    lhs.name == rhs.name && lhs.age == rhs.age
  }
}

Main (requires Swift 6.2 or later)

A common case is to have a @MainActor isolated type, SwiftUI views being a common example. Previously, the generated Equatable conformance had to be nonisolated in order to satisfy the protocol requirement. This would then restrict us to access only nonisolated properties of the type in the generated Equatable function — which meant that we had to ignore all @MainActor isolated properties in the equality comparison. Swift 6.2 introduced [isolated conformances]( Documentation ) allowing us to generate Equatable conformances which are bound to the @MainActor. In this way the generated Equatable conformance can access @MainActor isolated properties of the type synchronously and the compiler will guarantee that the conformance will be called only from the @MainActor context.

We can do so by specifying @Equatable(isolation: .main), e.g: `


@Equatable(isolation: .main)
@MainActor
struct Person {
    let name: String
    let age: Int
}


expands to:

extension Person: @MainActor Equatable {
    public static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.name == rhs.name && lhs.age == rhs.age
    }
}

3 Likes