根据未过滤的数组过滤 SwiftUI returns 中 ForEach 中的 @Binding 数组 var

Filtering a @Binding array var in a ForEach in SwiftUI returns values based on unfiltered array

我是一名 Windows C# 开发人员,iOS/SwiftUI 开发新手,我想我自己在这里遇到了麻烦。

我有一个带有@Binding 变量的视图:

struct DetailView: View {
    @Binding var project: Project

该项目是一个包含一组任务的对象。我正在遍历项目的任务以显示其名称和一个切换,其状态由任务的变量 isComplete 确定。

    ForEach(filteredTasks.indices, id: \.self) { idx in
        HStack {
            Text(filteredTasks[idx].phase)
                .font(.caption)
            Spacer()
            Text(filteredTasks[idx].name)
            Spacer()
            Toggle("", isOn: self.$filteredTasks[idx].isComplete)
        }
    }
}

我花了很长时间才找到这段代码,我发现我必须按照一个带有 'indices' 选项的示例来让切换器单独处理每个任务,并且以确保其 isComplete 值已保存。

接下来,我想根据具有 Planning、Construction 或 Final 值的 Task 变量 phase 过滤任务列表。所以我创建了 4 个按钮(每个阶段一个,然后是一个 'All Tasks' 返回完整的、未过滤的列表),并且经过大量的反复试验(创建不再正确绑定的过滤数组,等等) ., 等)我试过了,基本上只使用原始数组。

            List {
                ForEach(project.tasks.filter({ [=13=].phase.contains(filterValue) }).indices, id: \.self) { idx in
                    HStack {
                        Text(project.tasks[idx].phase)
                            .font(.caption)
                        Spacer()
                        Text(project.tasks[idx].name)
                        Spacer()
                        Toggle("", isOn: self.$project.tasks[idx].isComplete)
                    }
                }
            }

当然,这似乎有效,因为我可以做一个测试:

    func CreateTestArray() {
        let testFilterArray = project.tasks.filter({ [=14=].phase.contains(filterValue) })
    }

这将为我提供我想要的过滤列表。但是,在我的 ForEach 视图中,它无法正常工作,我不确定如何解决它。

例如,我有 128 个任务,其中 10 个的值为 'Final',当我使用按钮将 filterValue 设置为 Final 时,testFilterArray 实际上包含正确的 10 个任务 - 但在 ForEach查看我得到原始数组中的 前十个 任务(它们属于 'Planning' 类型 - 原始数组按 Planning/Construction/Final 排序);显然,尽管有过滤语句,ForEach 仍在处理原始数组。 Planning 按钮发送 filterValue = "Planning",我得到了正确的结果,因为我在原始数组中拥有的 20 个 Planning 任务的过滤器 returns 0-19 索引,并且因为它们是原始数组中的第一个数组,它 'appears' Planning 过滤器工作正常,但实际上它只是偶然工作,如果数组排序不同,它就不会。

有什么想法可以解决这个问题,以便我可以实际过滤这个数组,为数组中的每个项目正确显示 isComplete 切换,以及动态更新切换状态?我觉得我需要在这里再次从头开始,因为我让这些限制使我陷入了一个很小的 ​​Swift 角落。

谢谢!

更新: 谢谢@jnpdx 的快速回复 - 我绝对应该包含对象(我在下面列出)。然而,回顾我的对象定义,我想知道我是否在管理对象时犯了一个更基本的错误,这就是为什么我陷入了我所遇到的情况(即,在早期的迭代中我尝试了一些你的建议)。无论如何,我发布的对象是 'projects',它是我传递给项目列表视图的项目列表,然后该视图将单个项目传递给项目视图,然后该视图列出其中的任务特定项目。

我觉得你的回答让我做出了正确的决定,我只需要后退并查看那些对象 definitions/management,看看如何达到可以直接解决的情况。

任务:

 struct Task:  Identifiable, Codable {
    let id: UUID
    var phase: String
    var category: String
    var name: String
    var isComplete: Bool
    
    init(id: UUID = UUID(), phase: String, category: String, name: String, isComplete: Bool) {
        self.id = id
        self.phase = phase
        self.category = category
        self.name = name
        self.isComplete = isComplete
    }
}

项目:

 struct Project: Identifiable, Codable {
    
    var id: UUID
    var name: String
    var type: String
    var tasks: [Task]
    var isComplete: Bool

    
    init(id: UUID = UUID(), name: String, type: String, tasks: [Task] = [], isComplete: Bool) {
        self.id = id
        self.name = name
        self.type = type
        self.tasks = tasks
        self.isComplete = isComplete
    }

}

和项目模型:

 class ProjectData: ObservableObject {
    
    // code to access the json file is here
    
    // An accessible list of projects from the saved file
    @Published var projects: [Project] = []
    
    // load and save functions follow

更新: 谢谢,@jnpdx,正如您所说,我需要进行调整以使其在我的特定模型设计中发挥作用,您的解决方案在进行了调整后才起作用。以下是最终在我的案例中起作用的片段。

在我看来:

 List {
  ForEach(project.tasks.filter({ [=18=].phase.contains(filterValue) })) { task in
    HStack {
      Text(task.name)
      Toggle("", isOn: self.makeBinding(item: task))
      }
   }
}

调用函数:

     func makeBinding(item: Task) -> Binding<Bool> {
        let i = self.project.tasks.firstIndex { [=19=].id == item.id }!
        return .init(
            get: { self.project.tasks[i].isComplete },
            set: { self.project.tasks[i].isComplete = [=19=] }
        )
    }

让我们看看您的代码中的以下行:

ForEach(project.tasks.filter({ [=10=].phase.contains(filterValue) }).indices, id: \.self) { idx in

在第一部分中,您过滤 tasks 然后请求索引。我怀疑您希望它 return 类似于 [1, 5, 10, 11, 12],这意味着它们在数组中的原始位置。但是,实际上,你会得到一个像 [0,1,2,3,4] 这样的连续数组,因为它为你提供了新创建的数组的索引(filter 的结果)。

有几种方法可以解决这个问题,这也与您之前的 ForEach 有关。

执行 ForEach 并遍历 structs/objects 而不是索引更为惯用。你没有显示 Task 是由什么组成的,但假设是这样的:

struct Task : Hashable {
  var id = UUID()
  var name: String
  var phrase: String
  var isComplete: Bool
}

要对其进行迭代,您可以这样做:

ForEach(task, id: \.id) { task in 
  Text(task.name)
  Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id)) //explained later
}

我在评论中询问了 Project 的类型,因为我不完全清楚为什么它是 @Binding。好像可能是一个对象?如果它是一个视图模型,那会很好,您可以在那里处理您的 Toggle 逻辑。类似于:

class Project : ObservableObject {
    @Published var tasks : [Task] = [Task(name: "1", phrase: "phase", isComplete: false),Task(name: "2", phrase: "phase", isComplete: true),Task(name: "3", phrase: "phase2", isComplete: false)]

    var completedTasks : [Task] {
        return tasks.filter { [=13=].isComplete }
    }
    
    func taskCompletedBinding(id: UUID) -> Binding<Bool> {
        Binding<Bool>(get: {
            self.tasks.first(where: { [=13=].id == id})?.isComplete ?? false
        }, set: { newValue in
            self.tasks = self.tasks.map { t in
                if t.id == id {
                    var tCopy = t
                    tCopy.isComplete = newValue
                    return tCopy
                } else {
                    return t
                }
            }
        })
    }
}

您可以测试这样做是否有效:

struct ContentView: View {
    @ObservedObject var project = Project()
    
    var body: some View {
        ForEach(project.tasks, id: \.id) { task in
            Text(task.name)
            Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id))
        }
    }
}

如果 Project 是一个结构而不是一个对象,最好像我上面那样将它包装在 ObservableObject 视图模型中。