Equatable properties in @Observable classes
One of the main advantages of using @Observable models in SwiftUI is that the framework can track individual property accesses. When a view reads a property in its body, it only needs to reevaluate when that property changes, rather than whenever another part of the model is updated.
But whether an assignment is treated as a change can depend on the type of the property. When a custom type doesn't conform to Equatable, assigning a new value invalidates dependent views even if all of its stored values are the same.
This matters when an observable model receives frequent updates from an external source. Polling requests, asynchronous sequences and framework callbacks can all deliver a new value even when the underlying state hasn't changed. Without equality checking, each assignment can unnecessarily reevaluate views that depend on the property.
To understand why the property type matters, consider an observable model that stores a custom DeliveryProgress value.
struct DeliveryProgress {
var completedStops: Int
var totalStops: Int
}
@MainActor
@Observable
final class DeliveryModel {
var progress: DeliveryProgress
init(progress: DeliveryProgress) {
self.progress = progress
}
}
The @Observable macro expands the stored property into observation-aware accessors. The generated code includes overloads that decide whether an assignment should notify observers. In a simplified form, the relevant overloads look like this.
private func shouldNotifyObservers<Member>(
_ lhs: Member,
_ rhs: Member
) -> Bool {
true
}
private func shouldNotifyObservers<Member: Equatable>(
_ lhs: Member,
_ rhs: Member
) -> Bool {
lhs != rhs
}
Because DeliveryProgress doesn't conform to Equatable, its setter uses the unconstrained overload, which always returns true. Every assignment to progress is registered as a mutation, including an assignment where completedStops and totalStops are unchanged. This invalidates any view that read progress during its previous body evaluation.
Adding Equatable conformance changes which overload the generated setter uses.
struct DeliveryProgress: Equatable {
var completedStops: Int
var totalStops: Int
}
The setter can now compare the new value with the value already in storage. When they are equal, it performs the assignment without registering a mutation, so views that depend on progress are not invalidated.
Standard types such as String, Int and Bool already conform to Equatable, so observable properties using them get this behavior automatically. We need to add the conformance when storing our own value types in an observable model.
The same behavior extends to arrays of custom values because Array conforms to Equatable when its element type conforms to Equatable. If we store the full list of delivery stops in an observable model, adding Equatable conformance to DeliveryStop makes [DeliveryStop] equatable as well.
struct DeliveryStop: Equatable, Identifiable {
let id: UUID
var name: String
var isCompleted: Bool
}
@MainActor
@Observable
final class DeliveryRouteModel {
var stops: [DeliveryStop] = []
}
The generated setter can now compare the arrays element by element and avoid notifying dependent views when the elements and their order haven't changed. This applies when a new array is assigned to the property. In-place mutations, such as calling append(), use the property's generated modify accessor and still register a mutation. Equality checks over large collections have their own cost, but they can still be worthwhile when the model frequently receives unchanged data and updating the dependent views would require more work.
If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects. And my new book The SwiftUI Way helps you adopt recommended patterns, avoid common pitfalls, and use SwiftUI's native tools appropriately to work with the framework rather than against it.
For more resources on Swift and SwiftUI, check out my other books and book bundles.



