Current setup:
Models:
Book.swift
import SwiftUI
final class Book: ObservableObject {
@Published var games: [Game] = []
init() {
self.games = [Game(), Game(), Game()]
}
}
Game.swift
final class Game: ObservableObject, Identifiable, Equatable {
var id: UUID = UUID()
@Published var isSelected: Bool = false
init() {}
static func ==(lhs: Game, rhs: Game) -> Bool {
lhs.id == rhs.id
}
}
Views:
ContentView.swift
struct ContentView: View {
@StateObject var book: Book = Book()
var body: some View {
Games(games: $book.games)
}
}
Games.swift
struct Games: View {
@Binding var games: [Game]
var body: some View {
ForEach($games) { game in
Toggle("Game", isOn: game.isSelected)
.toggleStyle(CustomToggleStyle())
}
}
}
ToggleStyle:
CustomToggleStyle.swift
struct CustomToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
Button {
configuration.isOn.toggle()
} label: {
Text(configuration.isOn ? "On" : "Off")
}
}
}
Problem:
This setup renders 3 toggles. One for each Game in the Book object. When tapping a toggle the book.isSelected value correctly updates.
(Can be verified by attaching a didSet block to isSelected in Game; or games in Book)
However, the toggle style itself always renders "Off" and doesn't update its view.
Expected behaviour:
When tapping the toggles. I expect both the isSelected value to toggle, and the toggle view to correctly show "on" or "off" based on isSelected.
What I found out:
- When I remove the
Equatableprotocol onGame. It works as expected. Although I don't understand why.
final class Game: ObservableObject, Identifiable {
var id: UUID = UUID()
@Published var isSelected: Bool = false
init() {}
}
- When I render the
ForEachblock directly insideContentView. Omitting theGamesview and itsBinding var gamesproperty. It also works as expected. I also don't understand why.
struct ContentView: View {
@StateObject var book: Book = Book()
var body: some View {
ForEach($book.games) { game in
Toggle("Game", isOn: game.isSelected)
.toggleStyle(CustomToggleStyle())
}
}
}
Questions:
- Is there something wrong with my code why the current setup doesn't work?
- Why does removing the
Equatableprotocol fromGameaffect the toggle? - Why does working around the extra
Viewwith theBindingproperty work as expected?
Context:
- The current setup was tested in a
playground app. - This code used to work on IOS 16. The issue only happens since IOS 17.
Is literally telling SwiftUI to only render when the
idchanges. You need to include all variables unless you have a really really good reason.Also, each
ObservableObjectneeds to be wrapped, make sure eachgameuses something like.in a subview