Chris Schlitt
Chris Schlitt

Reputation: 142

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.

Upvotes: 2

Views: 3967

Answers (2)

malhal
malhal

Reputation: 30773

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.

Upvotes: 2

jnpdx
jnpdx

Reputation: 52625

No, you're not necessarily doing something against best practices. I think in SwiftUI, the concepts of data model storage and manipulation quickly become more complex than, for example, what Apple tends to show in its demo code. With a real app, with a single source of truth, like you seem to be using, you're going to have to come up with some ways to bind the data to your views.

One solution is to write Bindings with your own get and set properties that interact with your ObservableObject. That might look like this, for example:

struct TaskView : View {
    var taskIdentifier: String // Passed from parent view
    
    @EnvironmentObject private var taskStore: TaskStore
    
    private var taskBinding : Binding<Task> {
        Binding {
            taskStore.tasks[taskIdentifier] ?? .init(identifier: "", title: "", tags: [])
        } set: {
            taskStore.tasks[taskIdentifier] = $0
        }
    }
    
    var body: some View {
        TextField("Task title", text: taskBinding.title)
    }
}

If you're averse to this sort of thing, one way to avoid it is to use CoreData. Because the models are made into ObservableObjects by the system, you can generally avoid this sort of thing and directly pass around and manipulate your models. However, that doesn't necessarily mean that it is the right (or better) choice either.

You may also want to explore TCA which is an increasingly popular state management and view binding library that provides quite a few built-in solutions for the type of thing you're looking at doing.

Upvotes: 3

Related Questions