3

I was following along with this lecture when I ran into some problems with my Observed Object updating. I have an @ObservedObject called EmojiMemoryGame with a published MemoryGame<String> variable called model. 'MemoryGame' is a struct that stores an array of cards, which each have a Bool variable that stores whether they are face up or not.

My ContentView is a View that shows each card in a grid on screen. When the user taps the card, viewModel.choose(card) toggles the isFaceUp variable of the card; the problem is that this does not cause the card to flip over on screen. This is my code in ContentView.swift:

struct ContentView: View {
    @ObservedObject var viewModel: EmojiMemoryGame
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 89))]) {
                ForEach(viewModel.cards) { card in
                    CardView(card: card)
                        .aspectRatio(2/3, contentMode: .fit)
                        .onTapGesture {
                            viewModel.choose(card)
                        }
                }
            }
        }
        .foregroundColor(.red)
        .padding(.horizontal)
    }
}

struct CardView: View {
    let card: MemoryGame<String>.Card
    
    var body: some View {
        ZStack {
            let shape = RoundedRectangle(cornerRadius: 20)
            
            if card.isFaceUp {
                shape
                    .fill()
                    .foregroundColor(.white)
                shape
                    .strokeBorder(lineWidth: 3)
                Text(card.content)
                    .font(.largeTitle)
            } else {
                shape
                    .fill()
            }
        }
    }
}

However, the code works as expected if I instead just copy/paste the code from CardView directly into the body of ContentView (see below), so I'm not really sure what is going on here.

struct ContentView: View {
    @ObservedObject var viewModel: EmojiMemoryGame
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 89))]) {
                ForEach(viewModel.cards) { card in
                    ZStack {
                        let shape = RoundedRectangle(cornerRadius: 20)
                        
                        if card.isFaceUp {
                            shape
                                .fill()
                                .foregroundColor(.white)
                            shape
                                .strokeBorder(lineWidth: 3)
                            Text(card.content)
                                .font(.largeTitle)
                        } else {
                            shape
                                .fill()
                        }
                    }
                    .aspectRatio(2/3, contentMode: .fit)
                    .onTapGesture {
                        viewModel.choose(card)
                    }
                }
            }
        }
        .foregroundColor(.red)
        .padding(.horizontal)
    }
}

Edit: Here is my EmojiMemoryGame.swift:

class EmojiMemoryGame: ObservableObject {
    static let emojis = ["", "", "‍️", "", "", "", "", "", "", "", "", "", "", "", "", "‍", "", "", "", ""]
    
    static func createMemoryGame() -> MemoryGame<String> {
        MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
            emojis[pairIndex]
        }
    }

    @Published var model: MemoryGame<String> = createMemoryGame()

    var cards: Array<MemoryGame<String>.Card> {
        return model.cards
    }
    
    func choose(_ card: MemoryGame<String>.Card) {
        model.choose(card)
    }
}

and MemoryGame.swift:

struct MemoryGame<CardContent> where CardContent: Equatable {
    
    struct Card: Identifiable, Equatable {
        var isFaceUp: Bool = false
        var isMatched: Bool = false
        var content: CardContent
        
        var id: Int
        
        static func == (lhs: MemoryGame<CardContent>.Card, rhs: MemoryGame<CardContent>.Card) -> Bool {
            lhs.id == rhs.id
        }
        
    }
    
    private(set) var cards: Array<Card>
        
    init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
        cards = Array<Card>()
        // add numberOfPairsOfCards * 2 cards to cards array
        for pairIndex in 0..<numberOfPairsOfCards {
            let content: CardContent = createCardContent(pairIndex)
            cards.append(Card(content: content, id: pairIndex*2))
            cards.append(Card(content: content, id: pairIndex*2 + 1))
        }
    }
    
    mutating func choose(_ card: Card) {
        let chosenIndex = cards.firstIndex(of: card)
        cards[chosenIndex!].isFaceUp.toggle()
    }
    
}
  • Did you try using `@StateObject` when first initializing `EmojiMemoryGame`? – Ben Myers Jun 30 '22 at 00:40
  • Need code where you have @Published and model init – cora Jun 30 '22 at 00:56
  • @BenMyers yes, unfortunately I'm still running into the same problem :/ – tweezerticle Jun 30 '22 at 00:58
  • Is it possible to add the .onTapGesture to your CardView? For example, at the bottom of your ZStack in your card view, add the onTapGesture. From my experience, onTapGesture does not seem to play nicely with For loops. – nickreps Jun 30 '22 at 02:39
  • 1
    You need to change MemoryGame to class and make isFaceUp published. You don't have a publisher set up to propagate the change. – cora Jun 30 '22 at 02:45
  • @nickreps I tried adding the .onTapGesture to my CardView but unfortunately this did not fix it – tweezerticle Jun 30 '22 at 03:02
  • @cora I tried making MemoryGame and Card classes and publishing isFaceUp but it's still not working. I think the problem is probably something in my ContentView.swift file because my code works when I get rid of the CardView struct and paste the contents into ContentView directly. – tweezerticle Jun 30 '22 at 03:20
  • If I copy the code above into Xcode, will it work? Or are there missing pieces needed to get it running on my end? If easier, you can upload to GitHub and I can clone and help diagnose! – nickreps Jun 30 '22 at 03:22

1 Answers1

4

The ForEach does not detect that any of cards changes because it is uses Equatable which in your case uses only id.

Here is a fix:

struct Card: Identifiable, Equatable {
    var isFaceUp: Bool = false
    var isMatched: Bool = false
    var content: CardContent

    var id: Int

    static func == (lhs: MemoryGame<CardContent>.Card, rhs: MemoryGame<CardContent>.Card) -> Bool {
        lhs.id == rhs.id && lhs.isFaceUp == rhs.isFaceUp   // << here !!
    }
}

and also needed update for

mutating func choose(_ card: Card) {
    let chosenIndex = cards.firstIndex{ $0.id == card.id } // << here !!
    cards[chosenIndex!].isFaceUp.toggle()
}

Tested with Xcode 13.4 / iOS 15.5

Asperi
  • 228,894
  • 20
  • 464
  • 690