swiftuiproperty-binding

Passing property per reference to a DisclosureGroup in SwiftUI


I am trying to create a Google Task app with SwiftUI. I would like to have the Google Task Lists and the associated Tasks as an Outline/Treeview in a sidebar. I thought in the sidebar the task list can be expanded to tasks with a DisclosureGroup. And every task within the DisclosureGroup consists of a toggle button (task name & checked). Since the data model are tasklists, where each tasklist consists of tasks, which have names, date & time, checked-status, ..., I do not know how the property "checked" of the task is referenced within the toggle button of the DisclosureGroup. The Toggle button needs a bindable Bool.

Here the view (the @State var checked does not help, because all tasks are then either checked or unchecked together. I need it per task)

    struct ContentView: View {

    @ObservedObject var viewModel = TaskList()
    @State var checked: Bool = false

    var body: some View {
        
        List {
            ForEach(viewModel.tasklists) { tasklist in
                DisclosureGroup(tasklist.name) {
                    ForEach(tasklist.tasks ?? []) { task in
                        Toggle(task.name, isOn: $checked) <-- How can I get a reference to the model
                    }
                }
            }
        }
        .padding()
    }

and here the model:

class TaskList: ObservableObject {
    @Published var tasklists = [
        GoogleTasklist(name: "Tasklist 1", tasks: [GoogleTask(name: "Task 11"), GoogleTask(name: "Task 12")]),
        GoogleTasklist(name: "Tasklist 2", tasks: [GoogleTask(name: "Task 21"), GoogleTask(name: "Task 22")]),
        GoogleTasklist(name: "Tasklist 3", tasks: [GoogleTask(name: "Task 31"), GoogleTask(name: "Task 32")]),
        GoogleTasklist(name: "Tasklist 4", tasks: [GoogleTask(name: "Task 41"), GoogleTask(name: "Task 42")])
    ]
}

class GoogleTask: Identifiable, Equatable {
    static func == (lhs: GoogleTask, rhs: GoogleTask) -> Bool {
        lhs.id == rhs.id
    }
    
    let name: String
    var checked = false <--- This property should be changed by the Toggle button
    let id = UUID()
    
    init(name: String) {
        self.name = name
    }
}

class GoogleTasklist: Identifiable, Equatable {
    static func == (lhs: GoogleTasklist, rhs: GoogleTasklist) -> Bool {
        lhs.id == rhs.id
    }
    
    let name: String
    var tasks: [GoogleTask]?
    let id = UUID()
    
    init(name: String) {
        self.name = name
    }
    
    init(name: String, tasks: [GoogleTask]) {
        self.name = name
        self.tasks = tasks
    }
}

Any idea?


Solution

  • The issues you are having with the DisclosureGroup are the result of a few wrong choices along the way of getting there. Let's start with your view model:

    class GoogleTasklist: Identifiable, Equatable {
        static func == (lhs: GoogleTasklist, rhs: GoogleTasklist) -> Bool {
            lhs.id == rhs.id
        }
        
        let name: String
        var tasks: [GoogleTask] // Make tasks non-optional
        let id = UUID()
        
        init(name: String) {
            self.name = name
            tasks = [] // Initialize it to an empty array
        }
        
        init(name: String, tasks: [GoogleTask]) {
            self.name = name
            self.tasks = tasks
        }
    }
    

    In GoogleTasklist, you made tasks optional. That causes you trouble in your view when you had this code: ForEach(tasklist.tasks ?? []). Instead, make it non-optional and init it as []. That let's you change the prior code to ForEach(tasklist.tasks). I have yet to run across a good use case for an optional array. It can always be simply empty.

    Now we can turn to your view:

    struct ContentView: View {
        @ObservedObject var viewModel = TaskList()
    
        var body: some View {
            
            List {
                ForEach($viewModel.tasklists) { $tasklist in
                    DisclosureGroup(tasklist.name) {
                        ForEach($tasklist.tasks) { $task in
                            Toggle(task.name, isOn: $task.checked)
                        }
                    }
                }
            }
            .padding()
        }
    }
    

    Since you want to change the Bool in your view model, let's use it instead of using @State var checked. There are a few issues with using the state var. First, you are trying to model the checked state of an array with one variable. That won't work. Second, you still need to get the choice into your model.

    Instead, we can use the view model itself. All you need to do is pass the model with a $, and you can use can access the model throughout. If you need to provide a Binding, just use $, else you get the unwrapped version such as I used to provide the text.