6

Using Swift 5.3, how can I implement the Identifiable protocol on a struct by having its identity depend on the combination of two enum variables?

The code in question is simple,

struct Card: Identifiable {
    let suit: Suit
    let rank: Rank
    
    enum Suit {
        case spades, clubs, diamonds, hearts
    }
    
    enum Rank: Int {
        case one = 1, two, three, four, five, six, seven, jack, queen, king
    }
}

The above struct does not conform to the Identifiable protocol yet. How can I implement its identity as being the unique combination of its suit and rank (which are only created once)? Essentially, its identity could be 'spades-1' or 'diamonds-jack'. Furthermore, if possible I would like to retain the rank as an Int type, to allow for arithmetic later. Thank you in advance!

mfaani
  • 33,269
  • 19
  • 164
  • 293
Isaiah
  • 1,852
  • 4
  • 23
  • 48

2 Answers2

15

Since this type is exactly defined by the combination of its values, it is its own Identifier. So as long as Card is Hashable, it can identify itself:

extension Card: Hashable, Identifiable {
    var id: Self { self }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Hey Rob, would it be correct to say that this is generalizable to all structs with stable, public-only members? They're identity is defined by their value, unlike the identity of class instances – Alexander Aug 17 '20 at 19:33
  • 4
    Not in my opinion. Structs do not, by default, have any identity. Attaching Identifiable gives them one and says there is exactly one of this in this system. For example, in a deck there is exactly one King of Diamonds. If you see another, it is literally the *same card*, not just an equivalent/substitutable (i.e. Equatable) card. If that's not true, Identifiable was a lie. This has important impacts on ForEach loops (I see people use `id: \.self` all the time when the elements in the list are not guaranteed to be unique. It kind of "works" until it doesn't…) – Rob Napier Aug 17 '20 at 19:45
0

Conform Suit to String and combine them to create a unique identifier String.

struct Card: Identifiable {

    var id: String { "\(suit.rawValue)\(rank.rawValue)" }

    let suit: Suit
    let rank: Rank

    enum Suit: String {
        case spades, clubs, diamonds, hearts
    }
    enum Rank: Int {
        case one = 1, two, three, four, five, six, seven, jack, queen, king
    }
}

Caution: This can't be generalized for every scenario. Although this works fine in this particular one. Because we're using the concatenation of two strings together to compute the id of the Card's object.

Frankenstein
  • 15,732
  • 4
  • 22
  • 47
  • 1
    This works, but you have to be careful applying this in the general case. It's possible to accidentally introduce identifier collisions depending on the values your stitching together. Imagine there was a first name and last name field. `Person(firstName: "ab", lastName: "c")` would have the same identifier as `Person(firstName: "a", lastName: "bc")` – Alexander Aug 17 '20 at 19:07
  • 2
    @Alexander-ReinstateMonica Yes, this can't generalized. Although this work perfectly for this particular scenario. – Frankenstein Aug 17 '20 at 19:08
  • Yep, I'm just offering a word of caution. Because that would be one of those nasty types of logical errors that creeps in, makes subtle errors and never trips a compiler error. I didn't downvote, btw. – Alexander Aug 17 '20 at 19:14
  • 1
    Maybe this felt wrong for other members. Probably should have added a word of caution earlier. :) – Frankenstein Aug 17 '20 at 19:29
  • @Frankenstein, you could use a delimiter, like a dot `.`, to avoid this specific issue. You could also use hashValues instead of rawValue. – New Dev Aug 17 '20 at 19:34
  • 1
    @NewDev That's a possible approach to reduce errors. But it still isn't foolproof for scenarios like @Alexander has mentioned. `Person(firstName: "a.b", lastName: "c")` and `Person(firstName: "a", lastName: "b.c")` will have the same id. – Frankenstein Aug 17 '20 at 19:39
  • @Frankenstein, ah, yes - I didn't notice that it was both strings in his example. Well, hashValues would work, but then RobNapier's approach is the more elegant one – New Dev Aug 17 '20 at 19:46
  • 2
    @NewDev Yup, Rob's answer is the way to go. This is just an alternative for people who are looking for one. This has limitations and the limitations discussed here. So I think this is a good insight for others. That's why I'm not deleting this even at the cost of down-votes. – Frankenstein Aug 17 '20 at 19:51