Search code examples
swiftuiobservedobjectequatableswiftui-foreach

Why does SwiftUI's ForEach not update when the given data conforms to Equatable?


I'm following the I am following the CS193P Stanford class to learn SwiftUI after working with Storyboards for more than 3 years.

Already having a bit of experience I allowed myself to do things quickly and not exactly like the professor does. This lead me to discovering a behaviour that I can't understand.

Consider the following code:

struct ContentView: View {
    @ObservedObject var viewModel: EmojiMemoryGame
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
                ForEach(viewModel.cards) { card in
                    CardView(card: card)
                        .onTapGesture {
                            viewModel.choose(card)
                        }
                }
            }
        }
    }
}

Where a Card is defined like this:

struct Card: Identifiable, Equatable {
    static func == (lhs: MemoryGame<CardContent>.Card, rhs: MemoryGame<CardContent>.Card) -> Bool {
        lhs.id == rhs.id
    }
        
    var isFaceUp = true
    var isMatched = false
    var content: CardContent
    
    var id = UUID()
}

mutating func choose(_ card: Card) {
    guard let index = cards.firstIndex(where: { $0 == card }) else { return }
    cards[index].isFaceUp.toggle()
}

Why doesn't the view update when the viewModel.cards array is updated? If I remove only the Equatable protocol, leaving the rest of the app untouched:

struct Card: Identifiable { //..

it works just fine.


Solution

  • Since the id never changes (you are initializing it with a UUID) and your == func is only looking at the id of each card to determine equality, the ForEach will never see any change in the other attributes. I had to write the == func as below, and it worked for me.

    static func == (lhs: MemoryGame.Card, rhs: MemoryGame.Card) -> Bool {
       return lhs.isFaceUp == rhs.isFaceUp &&
       lhs.isMatched == rhs.isMatched &&
       lhs.content == rhs.content &&
       lhs.id == rhs.id
    }