Swift tuples don't conform to Equatable. We use parameter packs to create a tuple-like struct with Equatable conformance.
Mathijs Kadijk, Tom Lokhorst
tldr; Parameter packs are great to create an Equatables
struct that wraps multiple values and conforms to Equatable, perfect for SwiftUI's onChange and similar APIs.
When building SwiftUI views, every now and then you might need to observe changes to multiple values simultaneously. My instinct is to use a tuple like (foo, bar)
in an onChange
modifier for this.
However Swift has a limitation where tuples don't automatically conform to Equatable, even when all their elements do. There is an accepted proposal from 2020 that still isn't implemented yet, so sadly this approach won't work.
Writing a specific wrapper struct is a bit annoying when you just have two equatable values and simply want to trigger an action when either changes. You could observe them separately with multiple onChange
calls, but that's also not ideal leads to duplicated code paths and might lead to potential synchronization issues.
If you support iOS 17+ only you can use the Swift parameter packs feature! This allows us to create a struct that accepts a variable number of parameters. We can declare we only accept Equatable
parameters and therefore are ourselves also Equatable
. This is a perfect way of building a tuple-like type that we can use instead of a regular tuple.
Here's the Equatables
struct we use throughout our apps:
@available(iOS 17.0, *)
@available(tvOS 17.0, *)
@available(macOS 14.0, *)
@available(watchOS 10.0, *)
@available(visionOS 1.0, *)
struct Equatables<each T: Equatable>: Equatable {
let values: (repeat each T)
init(_ values: repeat each T) {
self.values = (repeat each values)
}
static func == (lhs: Equatables, rhs: Equatables) -> Bool {
for isEqual in repeat each lhs.values == each rhs.values {
guard isEqual else { return false }
}
return true
}
}
The struct uses parameter packs (indicated by each T
) to accept any number of Equatable
types. The repeat each
syntax expands the pack to create a tuple of values internally. The equality operator in turn iterates through each pair of corresponding values, comparing them one by one—if any pair differs, the entire struct is considered unequal.
Here's how you'd use it in a SwiftUI view:
struct ContentView: View {
@State private var searchText = "Dog"
@State private var selectedCategory = .animals
var body: some View {
List {
// Your view content
}
.onChange(of: Equatables(searchText, selectedCategory)) { oldValues, newValues in
// This fires when either searchText OR selectedCategory changes
performSearch()
}
}
}
Instead of writing separate onChange
modifiers or creating a custom struct for each combination of values you want to observe, you can simply wrap them in Equatables
and pass them as a single unit.
This simple pattern eliminates a common pain point in SwiftUI development. By leveraging parameter packs, we get tuple-like convenience with proper Equatable conformance. However it would still be great if Swift would get automatic conformance to things like Equatable
and Hashable
. Let's hope that proposal will be implemented and we'll get this feature in a future version of the language!
Perfect for app demos & presentations; Simply plug in an iPhone and it automatically shows up on your Mac.