I have a strange behavior in my code. It's a simple app that shows four views (CardView) and each time one of them is tapped, the view will reflect it by changing its border (using "selected" property). The problem is that when "value" property is a String it doesn't work. However when "value" property is of type Int it does work. What is this behavior due to?
import SwiftUI
struct Deck {
var cards = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map {
// Card(value: $0)
Card(value: "\($0)")
}
}
struct Card: Identifiable {
let id = UUID()
// let value: Int // Using int value solves the problem...
let value: String // Using string value provokes card view not updated
var selected = false
}
extension Card: Equatable {
static func ==(lhs: Card, rhs: Card) -> Bool {
return lhs.id == rhs.id
}
}
final class TestVM: ObservableObject {
@Published var deck = Deck()
func toggleSelection(card: Card) {
let idx = deck.cards.firstIndex(of: card)!
deck.cards[idx].selected.toggle()
}
}
struct CardView: View {
@ObservedObject var testVM: TestVM
let card: Card
var body: some View {
Text("\(card.value)")
.frame(width: 40, height: 80)
.background {
Rectangle()
.stroke(card.selected ? .red : .black, lineWidth: 2)
}
.background(card.selected ? .red.opacity(0.2) : .gray.opacity(0.2))
.onTapGesture {
testVM.toggleSelection(card: card)
}
}
}
struct TestZStackView: View {
@StateObject var testVM = TestVM()
var body: some View {
HStack {
ForEach(testVM.deck.cards.prefix(4)) { card in
CardView(testVM: testVM, card: card)
}
}
}
}
struct TestZStackView_Previews: PreviewProvider {
static var previews: some View {
TestZStackView()
}
}
CodePudding user response:
Inside CardView
you display card
that is declared as let card: Card
,
meaning it will not change. When testVM.toggleSelection(card: card)
is called
the array of Card
in Deck
has not really changed, that is, no cards has been added or removed from it.
So the view is not updated.
Try this approach, works for me:
struct Deck {
var cards = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map {Card(value: "\($0)")}
}
struct Card: Identifiable {
let id = UUID()
let value: String
var selected = false
}
extension Card: Equatable {
static func ==(lhs: Card, rhs: Card) -> Bool {
return lhs.id == rhs.id
}
}
final class TestVM: ObservableObject {
@Published var deck = Deck()
}
struct CardView: View {
@Binding var card: Card // <-- here
var body: some View {
Text("\(card.value)")
.frame(width: 40, height: 80)
.background {Rectangle().stroke(card.selected ? .red : .black, lineWidth: 2)}
.background(card.selected ? .red.opacity(0.2) : .gray.opacity(0.2))
.onTapGesture {
card.selected.toggle() // <-- here
}
}
}
struct ContentView: View {
@StateObject var testVM = TestVM()
var body: some View {
HStack {
ForEach($testVM.deck.cards) { $card in // <-- here
if testVM.deck.cards.prefix(4).contains(card) {
CardView(card: $card) // <-- here
}
}
}
}
}
CodePudding user response:
I can't tell you why Int work but String doesn't, maybe Int for some strange reason changes so that even the outer view, TestZStackView
, gets redrawn.
What I have found though in your code is an inconsistency where you pass the Card
object to CardView
but what you update is not that instance but the Card
in your view models deck.cards
array.
So here is a solution where I pass the id
of each Card
to the detail view and then get the correct Card
from the view model instead.
Change in main view
ForEach(testVM.deck.cards.prefix(4)) { card in
CardView(testVM: testVM, id: card.id)
}
Change in detail view
@ObservedObject var testVM: TestVM
var id: Card.ID
var body: some View {
if let card = testVM.deck.cards.first(where: { $0.id == id}) {
Text("\(card.value)")
.frame(width: 40, height: 80)
.background {
Rectangle()
.stroke(card.selected ? .red : .black, lineWidth: 2)
}
.background(card.selected ? .red.opacity(0.2) : .gray.opacity(0.2))
.onTapGesture {
testVM.toggleSelection(card: card)
}
} else {
EmptyView()
}
}
There are probably other ways to either pass the actual card, like working with @Binding but the main point is that you need to keep track of your source of truth and update and display the same Card
objects.