Nina
Nina

Reputation: 1

SwiftUI ChildView Not recalculated on state change

So this is most likely a newbie SwiftUI question, I have a parent view which takes an @ObservedObject (e.g. viewModel) the view model seems to be correctly publishes changes in the model to the CollectionView, and view is correctly recalculating the body. However, the child view's (EmojiCell) body doesn't seem to be recalculated? Below is the related code, appreciate any insight. I can get this to work, if I change the Card to be ObservableClass and to have its isFaceUp as @Published but this obviously is not the right solution!

struct CollectionView: View {
  @ObservedObject var viewModel: EmojiMemoryGameViewModel
 
  init(viewModel: EmojiMemoryGameViewModel) {
    self.viewModel = viewModel
  }
  
  var body: some View {
    // This is called on tap as expected so the viewModel is indeed
    // correctly notifying the view of it's change, the body is
    // recalculated, BUT EmojiCell's body doesn't get called!
    print("CollectionView recalc")
    
    return HStack {
      ForEach(viewModel.cards) { card in
        // This doesn't
        EmojiCell(card: card).onTapGesture {
          viewModel.choose(card: card)
        }
        
        // This works!
//        return  ZStack {
//          if card.isFaceUp {
//            RoundedRectangle(cornerRadius: 10)
//              .fill(Color.white)
//            RoundedRectangle(cornerRadius: 10)
//              .stroke(lineWidth: 3)
//            Text(card.content)
//          }
//          else {
//            RoundedRectangle(cornerRadius: 10).fill()
//          }
//        }
//          .onTapGesture {
//            self.viewModel.choose(card: card)
//          }
      }
    }
    .padding()
    .foregroundColor(.orange)
    .font(.largeTitle)
    
  }
}


typealias EmojiCard = MemoryGame<String>.Card

struct EmojiCell: View {
  
  let card: EmojiCard
  
  var body: some View {
    print("Cell recalc")
    return ZStack {
      if card.isFaceUp {
        RoundedRectangle(cornerRadius: 10)
          .fill(Color.white)
        RoundedRectangle(cornerRadius: 10)
          .stroke(lineWidth: 3)
        Text(card.content)
      }
      else {
        RoundedRectangle(cornerRadius: 10).fill()
      }
    }
  }
}

// Other relevant code
  struct Card: Identifiable, Hashable {
    var id: Int
    var isFaceUp = true
    var isMatched = false
    var content: CardContent
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
  }
  

extension MemoryGame.Card: Equatable {
  static func == (lhs: MemoryGame<CardContent>.Card, rhs: MemoryGame<CardContent>.Card) -> Bool {
    return lhs.id == rhs.id
  }
}

class EmojiMemoryGameViewModel: ObservableObject {
  
  @Published private var model: MemoryGame<String> = EmojiMemoryGameViewModel.createMemoryGame()
  
  static func createMemoryGame() -> MemoryGame<String> {
    let listOfCards = ["👻", "👽", "👾"]
    return MemoryGame(numberOfPairsOfCards: listOfCards.count) { index in
      return listOfCards[index]
    }
  }
  
  var cards: [MemoryGame<String>.Card] {
    model.cards
  }
  
 
  func choose(card: MemoryGame<String>.Card) {
    model.choose(card)
  }
}



Upvotes: 0

Views: 91

Answers (1)

Nina
Nina

Reputation: 1

This was a silly mistake on my part. The problem was actually immature implementation of Equatable which is actually no longer needed as of Swift 4.1+ (same for Hashable). I originally added it to ensure I could compare Cards (needed for index(of:) method), however, since I was only checking comparing id's, it was messing with SwiftUI's internal comparison algorithm for redrawing. SwiftUI too was using my Equatable implementation and thinking oh the Card actually has not changed!

Upvotes: 0

Related Questions