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()
}
}
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