'Is it possible to create a binding property from a computed property in SwiftUI?
In my model data class I have a task array.
class Model: ObservableObject {
@Published var tasks: [Task] = Task.sampleData
}
In my HomeView I display these tasks however the user has the ability to use a picker to sort them by the date they were created or by the date they are due. In the HomeView I have an enum that contains the options that the user can sort them by and a state instance to hold the picked value. By default it is set to sort by due date.
@EnvironmentObject private var model: Model
@State private var sortOption: SortOption = .dueDate
enum SortOption {
case dueDate, creationDate
}
I decided to then create a computed value that contains the sorted array so I don't change the original tasks array as it is used to display tasks in other parts of the app.
var sortedTasks: [Task] {
model.tasks.sorted(by: {
if sortOption == .dueDate {
return $0.dueDate < $1.dueDate
} else {
return $0.creationDate < $1.creationDate
}
})
}
And soon after I ran into the problem. My Task Row requires a binding task and I am unable to retrieve the required binding from the computed property which was the sortedTasks array.
// ERROR: Cannot find '$sortedTasks' in scope
ForEach($sortedTasks) { $task in
TaskRow(task: $task)
}
My understanding is that if I want to keep the sortedTasks array then I will have to remove the binding property from the TaskRow but if I want to keep the binding property what should the approach be to display a sorted array without manipulating the tasks array in the data model class? Could the computed property return a sorted array of binding task objects?
Solution 1:[1]
You can´t do that but you can use a Combine approach here:
Move your sortedTasks, sortOption var to your class named Model and create a combined publisher from your two vars that should trigger the sortedTasks var to update. Sort the received collection and assign it to your sortedTasks.
class Model: ObservableObject {
@Published var tasks: [TaskModel] = TaskModel.sampleData
@Published var sortedTasks: [TaskModel] = TaskModel.sampleData
@Published var sortOption: SortOption = .dueDate
init(){
Publishers.CombineLatest($sortOption, $tasks)
.map{ sortOption, tasks in
// apply sorting here
}.assign(to: &$sortedTasks) // assign to your sorted var
}
}
Use it like:
var body: some View{
HStack{
ForEach($model.sortedTasks) { $task in
// TaskRow(task: $task)
}
}
}
I´ve renamed the Task model to TaskModel as i think it is a very bad idea to have a struct named after a system provided class.
Solution 2:[2]
The sort depends on sortOption which is view state so it is not correct to do it in the model because then you can't have different views using different sort options. The solution is to sort the bindings, not the tasks, e.g.
var sortedTasks: [Binding<Task>] {
$model.tasks.sorted { $x, $y in // $model.tasks.animation().sorted if you would like animations like List row moves.
if sortOption == .dueDate {
return x.dueDate < y.dueDate
} else {
return x.creationDate < y.creationDate
}
}
}
ForEach(sortedTasks) { $task in
TaskRow(task: $task)
}
You might be wondering how ForEach can find the id, well that's because Binding has @dynamicMemberLookup which means it forwards property lookups to its value.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | burnsi |
| Solution 2 |
