My main ContentView contains a ChecklistView, which contains several CheckViews, which contains a Toggle view. When clicking on a checkbox, the information is passed to the ContentViewModel, but does not come back to the subViews. How can I fix that? I'am using Xcode 16.3, with Swift 5 or Swift 6 : no difference. The joined code is designed to test different structures of view declaration.
import SwiftUI
@main
struct TestCheckListApp: App {
var body: some Scene {
WindowGroup {
ContentView(cvm: ContentViewModel())
}
}
}
import SwiftUI
final class ChecklistItem: Identifiable, ObservableObject
{
let id = UUID()
var title: String
var isChecked: Bool
init(title: String, isChecked: Bool = false) {
self.title = title
self.isChecked = isChecked
}
func toggle() { isChecked.toggle() }
}
import SwiftUI
final class ContentViewModel: ObservableObject {
@Published var report: String = ""
@Published var arrItem: [ChecklistItem] = [
ChecklistItem(title: "Item 0"),
ChecklistItem(title: "Item 1"),
ChecklistItem(title: "Item 2")
]
func buttonTapped()
{
var s: String = ""
for item in arrItem
{
s += "\(item.title) : \(item.isChecked), "
print("\(item.title) : \(item.isChecked)")
}
report = s
}
}
import SwiftUI
struct ContentView: View {
@ObservedObject var cvm: ContentViewModel
@State private var multiSelection = Set<UUID>()
var body: some View {
VStack(alignment: .leading) {
HStack {
Button(action: cvm.buttonTapped) {
Text("Display values in view model")
}
Text(cvm.report)
}
Divider()
HStack {
Text("Toggles in ContentView:")
Toggle(cvm.arrItem[0].title, isOn: $cvm.arrItem[0].isChecked)
Toggle(cvm.arrItem[1].title, isOn: $cvm.arrItem[1].isChecked)
Toggle(cvm.arrItem[2].title, isOn: $cvm.arrItem[2].isChecked)
}
Divider()
HStack {
Text("Toggles in CheckView in ContentView:")
CheckView(cli: cvm.arrItem[0])
CheckView(cli: cvm.arrItem[1])
CheckView(cli: cvm.arrItem[2])
}
Divider()
Text("Toggles in List in ContentView:")
List {
ForEach($cvm.arrItem) { item in
Toggle("item.title", isOn: item.isChecked) }
}
Divider()
Text("Toggles in ChecklistView:")
ChecklistView(cl: cvm.arrItem)
}
.padding()
.preferredColorScheme(.light)
}
}
#Preview {
ContentView(cvm: ContentViewModel())
}
import SwiftUI
struct CheckView: View {
@ObservedObject var cli: ChecklistItem
var body: some View {
Toggle(cli.title, isOn: $cli.isChecked)
}
}
#Preview {
CheckView(cli: ContentViewModel().arrItem[0])
}
import SwiftUI
struct ChecklistView : View {
var cl: [ChecklistItem]
var body: some View {
List {
ForEach(cl) { item in
CheckView(cli: item)
}
}
}
}
#Preview {
ChecklistView(cl: ContentViewModel().arrItem)
}
If you really want to keep using final class ContentViewModel: ObservableObject
,
then change your final class ChecklistItem
to a struct ChecklistItem
and ensure you pass a binding when you want to change it.
Also you need to declare a single source of data truth
for your ContentViewModel
.
Example code, tested on real device, not Previews:
@main
struct TestCheckListApp: App {
@StateObject private var cvm = ContentViewModel() // <-- here
var body: some Scene {
WindowGroup {
ContentView(cvm: cvm) // <-- here
}
}
}
struct ChecklistItem: Identifiable { // <-- here
let id = UUID()
var title: String
var isChecked: Bool
init(title: String, isChecked: Bool = false) {
self.title = title
self.isChecked = isChecked
}
}
final class ContentViewModel: ObservableObject {
@Published var report: String = ""
@Published var arrItem: [ChecklistItem] = [
ChecklistItem(title: "Item 0"),
ChecklistItem(title: "Item 1"),
ChecklistItem(title: "Item 2")
]
func buttonTapped()
{
var s: String = ""
for item in arrItem
{
s += "\(item.title) : \(item.isChecked), "
print("\(item.title) : \(item.isChecked)")
}
report = s
}
}
struct ContentView: View {
@ObservedObject var cvm: ContentViewModel
@State private var multiSelection = Set<UUID>()
var body: some View {
VStack(alignment: .leading) {
HStack {
Button(action: cvm.buttonTapped) {
Text("Display values in view model")
}
Text(cvm.report)
}
Divider()
HStack {
Text("Toggles in ContentView:")
Toggle(cvm.arrItem[0].title, isOn: $cvm.arrItem[0].isChecked)
Toggle(cvm.arrItem[1].title, isOn: $cvm.arrItem[1].isChecked)
Toggle(cvm.arrItem[2].title, isOn: $cvm.arrItem[2].isChecked)
}
Divider()
HStack {
Text("Toggles in CheckView in ContentView:")
CheckView(cli: $cvm.arrItem[0]) // <-- here
CheckView(cli: $cvm.arrItem[1])
CheckView(cli: $cvm.arrItem[2])
}
Divider()
Text("Toggles in List in ContentView:")
List {
ForEach($cvm.arrItem) { $item in // <-- here
Toggle("item.title", isOn: $item.isChecked) // <-- here
}
}
Divider()
Text("Toggles in ChecklistView:")
ChecklistView(cl: $cvm.arrItem) // <-- here
}
.padding()
.preferredColorScheme(.light)
}
}
struct CheckView: View {
@Binding var cli: ChecklistItem // <-- here
var body: some View {
Toggle(cli.title, isOn: $cli.isChecked) // <-- here
}
}
struct ChecklistView : View {
@Binding var cl: [ChecklistItem] // <-- here
var body: some View {
List {
ForEach($cl) { $item in // <-- here
CheckView(cli: $item) // <-- here
}
}
}
}