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()
    }
    
}
8
  • Did you try using @StateObject when first initializing EmojiMemoryGame? Commented Jun 30, 2022 at 0:40
  • Need code where you have @Published and model init Commented Jun 30, 2022 at 0:56
  • @BenMyers yes, unfortunately I'm still running into the same problem :/ Commented Jun 30, 2022 at 0: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. Commented Jun 30, 2022 at 2: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. Commented Jun 30, 2022 at 2:45

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.