Reputation: 142
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
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
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 Binding
s 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 ObservableObject
s 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