I'm building a small SwiftUI view with a list of CardView items that use a shared @Binding from an @Observable model. When I tap one button, all the items in the ForEach are re-rendered. I'm trying to understand why and how to prevent that.
This is my simple code:
import SwiftUI
struct GameObservableView: View {
@Observable final class Game {
let data = ["jxrfkzqp", "duwlemci", "hbpokxra", "tcyqdsne", "lvgurxzt"]
var isSelected: String?
}
@State private var game = Game()
var body: some View {
ForEach(game.data, id: \.self) { row in
VStack {
CardView(title: row, isSelected: $game. isSelected)
.padding()
}
}
}
}
struct CardView: View {
let title: String
@Binding var isSelected: String?
var body: some View {
let _ = print("update view \(title)")
Button {
withAnimation(.easeInOut(duration: 0.2)) {
if isSelected == title {
isSelected = nil
} else {
isSelected = title
}
}
} label: {
Text(title)
.font(.headline)
.frame(maxWidth: .infinity)
}
}
}
import SwiftUI
@main
struct CardsGameApp: App {
var body: some Scene {
WindowGroup {
GameObservableView()
}
}
}
Unfortunately, even after wrapping the logic inside a class (and the same happens without it), clicking the button still triggers the 'update view' print five times.
UPDATE: I noticed is that after making CardView conform to Equatable and removing the binding—moving the logic into the onTap closure—I managed to reduce the print output to one call when a single button is tapped, and two calls when two buttons need to be refreshed.
struct GameObservableView: View {
@State private var data = ["jxrfkzqp", "duwlemci", "hbpokxra", "tcyqdsne", "lvgurxzt"]
@State private var selectedTitle: String?
var body: some View {
VStack(spacing: 12) {
ForEach(data, id: \.self) { title in
CardView(
title: title,
isSelected: selectedTitle == title
) {
withAnimation(.easeInOut(duration: 0.2)) {
selectedTitle = (selectedTitle == title) ? nil : title
}
}
}
}
.padding()
}
}
struct CardView: View, Equatable {
let title: String
var isSelected: Bool
let onTap: () -> Void
var body: some View {
let _ = print("update view \(title)")
Button(action: onTap) {
Text(title)
.font(.headline)
.frame(maxWidth: .infinity)
}
}
static func == (lhs: CardView, rhs: CardView) -> Bool {
return lhs.title == rhs.title &&
lhs.isSelected == rhs.isSelected
}
}
@Statewith@Observable class. See also Managing model data in your app. You can see that you are looping over multiple data, but you only access one and the samegamein theCardViewcalls. You change one, and you change them all in the loop, hence your View updates. Look also at@Bindablewhen you need to use a binding.@Observable final class Gameinside the View makes it local to that type. You can’t reuse it elsewhere, test it, or share it between views. And yet this is the main purpose of using a@Observable class.GameObservableView.Gameelsewhere. Sure, it’s unwieldy, but it’s not private.