SwiftUI:绑定到从环境 object 派生的结构的 属性

SwiftUI: Binding on property of struct derived from environment object

我的任务管理器应用程序中有两个结构:

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

然后我有一个 class 来存储它们:

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

我将其作为 .environmentObject(taskStore).

传递给我的根视图

如果以下任何错误(反对不良做法),请纠正我:

在我的 TaskView 我有:

    @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
        }
    }

问题是,在学习 SwiftUI 时,我被告知在制作某些组件时(比如在这种情况下让你改变标签数组的选择器)它应该接受对 value/collection 的绑定,或者说我想让任务标题可编辑,我需要绑定到 task.title 属性,这两点我都做不到,因为(基于我定义和计算的方式 task) 我无法绑定 task.

我是不是在做一些违反最佳实践的事情?或者在这条道路上,我在哪里偏离了在环境中存储真实点的正确方法object,并使它们在子视图中可编辑。

不,您不一定在做违反最佳实践的事情。我认为在 SwiftUI 中,数据模型存储和操作的概念很快变得比 Apple 倾向于在其演示代码中展示的内容更加复杂。对于真正的应用程序,具有单一事实来源,就像您似乎正在使用的那样,您将不得不想出一些方法将数据绑定到您的视图。

一种解决方案是使用您自己的 getset 属性编写 Binding,这些属性与您的 ObservableObject 交互。这可能看起来像这样,例如:

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] = [=10=]
        }
    }
    
    var body: some View {
        TextField("Task title", text: taskBinding.title)
    }
}

如果您不喜欢这种事情,避免这种情况的一种方法是使用 CoreData。因为模型是由系统做成ObservableObject的,所以你一般可以避免这种事情,直接传递和操作你的模型。然而,这并不一定意味着它是正确(或更好)的选择。

您可能还想探索 TCA,这是一个越来越受欢迎的状态管理和视图绑定库,它为您正在做的事情提供了很多 built-in 解决方案。

您使用值类型对数据建模并管理生命周期,side-effects 使用引用类型是正确的。您缺少的一点是 Task 没有实现 Identifiable 协议,该协议使 SwiftUI 能够跟踪 ListForEach 中的数据。按如下方式实施:

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

然后切换到使用数组,例如

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: { [=11=].id == identifier })
    }
}

现在您已经有了一组可识别的数据,通过以下方式绑定到任务真的很简单:

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

我建议查看 Apple's Fruta sample 了解更多详情。