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!

11 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.

1 Like

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. ↩︎

2 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.

5 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

1 Like