Passing property per reference to a DisclosureGroup in SwiftUI

99 Views Asked by At

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?

2

There are 2 best solutions below

2
Yrb On BEST ANSWER

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.

0
Money On

With the ideas of "lorem ipsum" and "Asperi" I managed to get it to work. But to be honest, it was more trial and error (where to set the $...). If I look back to C++, handling pointers were no more complicated.

So here is my solution:

The views:

struct ContentView: View {

    @ObservedObject var viewModel = TaskList()
    
    var body: some View {
        
        List {
            ForEach($viewModel.allTasklists) { $tasklist in
                DisclosureGroup(tasklist.name) {
                    ForEach($tasklist.tasks) { $task in
                        GoogleTaskView(task: $task)
                    }
                }
            }
        }
        .padding()
    }
}

struct GoogleTaskView: View {
    @Binding var task: GoogleTask
    
    var body: some View {
        Toggle(task.name, isOn: $task.checked)
    }
}

and the model

class TaskList: ObservableObject {
    @Published var allTasklists = [
        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")])
    ]
}

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

struct GoogleTask: Identifiable, Equatable {
    static func == (lhs: GoogleTask, rhs: GoogleTask) -> Bool {
        lhs.id == rhs.id
    }
    
    var name: String = ""
    var checked = false
    let id = UUID()
}