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 Equatable protocol on Game. 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 ForEach block directly inside ContentView. Omitting the Games view and its Binding var games property. 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:

  1. Is there something wrong with my code why the current setup doesn't work?
  2. Why does removing the Equatable protocol from Game affect the toggle?
  3. Why does working around the extra View with the Binding property 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.
1

There are 1 best solutions below

0
On
static func ==(lhs: Game, rhs: Game) -> Bool {
        lhs.id == rhs.id
    }

Is literally telling SwiftUI to only render when the id changes. You need to include all variables unless you have a really really good reason.

Also, each ObservableObject needs to be wrapped, make sure each game uses something like.

@ObservedObject var game: Game 

in a subview