How to Make a Swift Enum with Associated Values Equatable

How to make a Swift enum with associated values equatable

SE-0185 Synthesizing Equatable and Hashable conformance has been implemented in Swift 4.1, so that it suffices do declare conformance to the protocol (if all members are Equatable):

enum ViewModel: Equatable {
case heading(String)
case options(id: String, title: String, enabled: Bool)
}

For earlier Swift versions, a convenient way is to use that tuples can be compared with ==.

You many also want to enclose the compatibility code in a Swift version check, so that the automatic synthesis is used once the project is updated to Swift 4.1:

enum ViewModel: Equatable {
case heading(String)
case options(id: String, title: String, enabled: Bool)

#if swift(>=4.1)
#else
static func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
switch (lhs, rhs) {
case (let .heading(lhsString), let .heading(rhsString)):
return lhsString == rhsString
case (let .options(lhsId, lhsTitle, lhsEnabled), let .options(rhsId, rhsTitle, rhsEnabled)):
return (lhsId, lhsTitle, lhsEnabled) == (rhsId, rhsTitle, rhsEnabled)
default:
return false
}
}
#endif
}

Swift enum conformance to Equatable when result type used as associated value: Type doesn't conform to protocol Equatable

Enum can automatically conform to Equatable if its associated values conform to Equatable, from docs:

For an enum, all its associated values must conform to Equatable. (An enum without associated values has Equatable conformance even without the declaration.)

And the Result<Success, Failure> only conforms to Equatable when

Success conforms to Equatable, Failure conforms to Equatable, and Failure conforms to Error.

Your result's failure is only conform to Error and Error is not Equatable yet. You can try to replace your Error with a type that conforms to both Error and Equatable

enum BookAction: Equatable {
case dataResponse(Result<Book, ActionError>)
}

struct ActionError: Error, Equatable { }

Ref:

https://developer.apple.com/documentation/swift/equatable
https://developer.apple.com/documentation/swift/result

How to test equality of Swift enums with associated values


Swift 4.1+

As @jedwidz has helpfully pointed out, from Swift 4.1 (due to SE-0185, Swift also supports synthesizing Equatable and Hashable for enums with associated values.

So if you're on Swift 4.1 or newer, the following will automatically synthesize the necessary methods such that XCTAssert(t1 == t2) works. The key is to add the Equatable protocol to your enum.

enum SimpleToken: Equatable {
case Name(String)
case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)

Before Swift 4.1

As others have noted, Swift doesn't synthesize the necessary equality operators automatically. Let me propose a cleaner (IMHO) implementation, though:

enum SimpleToken: Equatable {
case Name(String)
case Number(Int)
}

public func ==(lhs: SimpleToken, rhs: SimpleToken) -> Bool {
switch (lhs, rhs) {
case let (.Name(a), .Name(b)),
let (.Number(a), .Number(b)):
return a == b
default:
return false
}
}

It's far from ideal — there's a lot of repetition — but at least you don't need to do nested switches with if-statements inside.

How do I know the type of an enum with associated value in a generic way?

You just need to make your enumeration conform to Equatable and compare the associated values for equality:

enum MyEnum: Equatable {
case one
case two
case three(user: String)
}


var array: [MyEnum] = []
func add(_ value: MyEnum) {
if array.contains(value) { return }
array.append(value)
}


add(.one)
add(.one)
array
add(.three(user: "a"))
add(.three(user: "b"))
add(.three(user: "a"))

array[0] // one
array[1] // three(user: "a")
array[2] // three(user: "b")

To check if the last element is of case .three:

if case .three = array.last {
print(true)
}

Automatic synthesis of Equatable conformance for Swift struct or enum through an extension

The proposal (SE-0185) for the synthesis says something different to the documentation you linked:

Users must opt-in to automatic synthesis by declaring their type as Equatable or Hashable without implementing any of their requirements. This conformance must be part of the original type declaration or in an extension in the same file (to ensure that private and fileprivate members can be accessed from the extension).

According to the proposal, declaring conformance in extensions in the same file also automatically generates the required members, which is in line with the actual behaviour. If you declare the extension in a different file from the type, you should see an error message:

Extension outside of file declaring struct 'Point' prevents automatic synthesis of '==' for protocol 'Equatable'

Making an enum with associated values conform to decodable?

You need to create a custom init(from decoder: Decoder) throws, and treat your associated values like they are normal fields in the object, using CodingKeys to decode them. Details depend on how your associated values are actually represented in encoded object.

For example if your values are encoded as:

{ "team1": "aaa", "team2": "bbb" }

and

{ "individual": ["x", "y", "z"]}

You can:

enum SportType: Decodable {

case team(String, String) //Two team names
case individual([String]) //List of player names

// Define associated values as keys
enum CodingKeys: String, CodingKey {
case team1
case team2
case individual
}

init(from decoder: Decoder) throws {

let container = try decoder.container(keyedBy: CodingKeys.self)

// Try to decode as team
if let team1 = try container.decodeIfPresent(String.self, forKey: .team1),
let team2 = try container.decodeIfPresent(String.self, forKey: .team2) {
self = .team(team1, team2)
return
}

// Try to decode as individual
if let individual = try container.decodeIfPresent([String].self, forKey: .individual) {
self = .individual(individual)
return
}

// No luck
throw DecodingError.dataCorruptedError(forKey: .individual, in: container, debugDescription: "No match")
}
}

Test:

let encoded = """
{ "team1": "aaa", "team2": "bbb" }
""".data(using: .utf8)

let decoded = try? JSONDecoder().decode(SportType.self, from: encoded!)

switch decoded {
case .team(let a, let b):
print("I am a valid team with team1=\(a), team2=\(b)")
case .individual(let a):
print("I am a valid individual with individual=\(a)")
default:
print("I was not parsed :(")
}

Prints:

I am a valid team with team1=aaa, team2=bbb

Swift Enum Equatable expected argument issue

One simple way to test if a variable is of type .poules is to move the actual comparison inside a computed property of the enum. This also makes sense since this comparison isn't really what you would use for Equatable

var isAnyPoules: Bool {
if case .poules = self { return true }

return false
}

This will make it easier to perform the check

if router.currentView.isAnyPoules 

or

.transition(viewRouter.originView.isAnyPoules ? X : X)


Related Topics



Leave a reply



Submit