CaptureContext/swift-equated
Equatable wrapper type and a set of basic comparators
Table of Contents
- The problem - The solution - Features - Predefined comparators - Installation - Basic - Recommended - License
Motivation
Swift strongly encourages Equatable, and for good reason. But in practice, equality often disappears at API boundaries:
- values stored as
any - errors erased to
Swift.Error - closures, reference types, or foreign types
- generic code that cannot add
Equatableconstraints
The problem
A very common example of this appears when using TCA, though the issue is by no means specific to it.
In TCA it’s idiomatic to make Actions equatable for better testing, diffing, and debugging. But the moment you want to carry an error, you hit a wall:
enum Action: Equatable {
case requestFailed(Error) // ❌ 'Error' does not conform to 'Equatable'
}A typical workaround is to introduce a bespoke wrapper type that forces equatability by comparing something like localizedDescription:
struct EquatableError: LocalizedError, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.localizedDescription == rhs.localizedDescription
}
let underlyingError: Error
var localizedDescription: String {
underlyingError.localizedDescription
}
}
enum FeatureAction: Equatable {
case requestFailed(EquatableError) // ✅ compiles
}This works, but it comes with tradeoffs:
- a new wrapper type must be declared
- an equality strategy must be chosen and documented
- the same pattern is often repeated across features and modules
And the “right” definition of equality is frequently context-dependent.
The solution
Equated lets you keep actions equatable without defining custom wrapper types:
import Equated
enum FeatureAction: Equatable {
case requestFailed(Equated<Error>) // ✅ compiles
}For errors, it’s enough to simply wrap the value:
return .send(.requestFailed(Equated(error)))By default, Equated chooses an appropriate equality strategy:
- if the underlying error can be compared using
Equatable,==is used - otherwise it falls back to comparing
localizedDescription
[!NOTE] Comparing
localizedDescriptionis a heuristic. It is common, but not guaranteed to be unique or stable across localization changes.
Equality can always be customized explicitly when needed:
return .send(.requestFailed(Equated(error, by: .property(\.code))))Features
Equated is a lightweight Equatable container that lets you define equality explicitly, while keeping call sites terse.
Predefined comparators
Choose how two values should be compared using a Equated.Comparator:
Automatic
The detectEquatable comparator attempts to cast values to any Equatable and compare them using ==:
.detectEquatable(
checkBoth: Bool = false,
fallback: Comparator = .dump
)If equatable comparison is not possible, the provided fallback comparator is used.
Building blocks
.const(Bool)– always equal / never equal.custom((Value, Value) -> Bool)– full control.dump– compares the textualdump()output
Equatable-driven
.defaultEquatable– equivalent to using==directly
Property-based
The property comparator compares values by a derived equatable projection:
.property(\.someEquatableProperty)
.property { String(reflecting: $0) }.objectID– compare reference identity (only whenValue: AnyObject)
Error convenience
The .localizedDescription comparator is equivalent to .property(\.localizedDescription).
It is typically most useful as a fallback, for example:
.detectEquatable(fallback: .localizedDescription)Concurrency escape hatches
.uncheckedSendable((Value) -> any Equatable)
A property-style comparator for non-sendable projections
.uncheckedSendable((Value, Value) -> Bool)
A custom-style comparator for non-sendable values
[!NOTE]
Most users should prefer
.detectEquatable()or.propertycomparators
Installation
Basic
You can add swift-equated to an Xcode project by adding it as a package dependency.
- From the File menu, select Swift Packages › Add Package Dependency…
- Enter
"https://github.com/capturecontext/swift-equated"into the package repository URL text field - Choose products you need to link to your project.
Recommended
If you use SwiftPM for your project structure, add swift-equated dependency to your package file
.package(
url: "https://github.com/capturecontext/swift-equated.git",
.upToNextMinor("0.0.5")
)Do not forget about target dependencies
.product(
name: "Equated",
package: "swift-equated"
)License
This library is released under the MIT license. See LICENSE for details.
Package Metadata
Repository: CaptureContext/swift-equated
Stars: 0
Forks: 0
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
Topics: equatable, foundation, swift
README: README.md