'SwiftUI: Binding on property of struct derived from environment object

I have two structs in my task manager app:

struct Task {
    var identifier: String
    var title: String
    var tags: [String] // Array of tag identifiers
}
struct Tag {
    var identifier: String
    var title: String
}

I then have a class to store them:

class TaskStore: ObservableObject {
    @Published var tasks = [String:Task]()
    @Published var tags = [String:Tag]()
}

which I pass to my root view as an .environmentObject(taskStore).

Correct me if any of the following is wrong (against bad practices):

In my TaskView I have:

    @EnvironmentObject var taskStore: TaskStore

    var taskIdentifier: String // Passed from parent view

    private var task: Task {
        get {
            return taskStore.tasks[taskIdentifier]! // Looks up the task in the store
        }
    }
    private var tags: [Tag] {
        get {
            return taskStore.tags
        }
    }

The issue is, when learning SwiftUI I was told when making certain components (like a picker that let's you alter the tags array in this case) that it should accept a binding to the value/collection, or say I want to make the task title editable, I need a binding to the task.title property, both of which I can't do because (based on the way I'm defining and computing task) I can't get a binding on task.

Am I doing something against best practices here? Or where along this path did I diverge from the right way of storing points of truth in an environment object, and make them editable in sub views.



Solution 1:[1]

You are correct to model your data with value types and manage lifecycle and side-effects with a reference type. The bit you are missing is that Task doesn't implement the Identifiable protocol which enables SwiftUI to keep track of the data in a List or ForEach. Implement that as follows:

struct Task: Identifiable {
    var id: String
    var title: String
    var tags: [String] // Array of tag identifiers
}

Then switch to using an array, e.g.

class TaskStore: ObservableObject {
    @Published var tasks = [Task]()
    @Published var tags = [Tag]()

    // you might find this helper found in Fruta useful
    func task(for identifier: String) -> Task? {
        return tasks.first(where: { $0.id == identifier })
    }
}

Now that you have an array of identifiable data it is real simple to get a binding to the task via:

List($model.tasks) { $task in 
    // now you have a binding to the task
}

I recommend checking out Apple's Fruta sample for more detail.

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