在 SwiftUI 中的 ForEach 中过滤 @Binding 数组 var 返回基于未过滤数组的值

sid*_*sid 6 arrays filter indices swiftui

我是一名 Windows C# 开发人员,刚接触 iOS/SwiftUI 开发,我想我已经陷入了困境。

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

struct DetailView: View {
    @Binding var project: Project
Run Code Online (Sandbox Code Playgroud)

该项目是一个包含任务数组的对象。我循环遍历项目的任务以显示其名称和一个开关,其状态由任务的变量 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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我花了相当长的时间才完成这段代码,我发现我必须遵循带有“索引”选项的示例才能使切换单独处理每个任务,并确保其 isComplete 值被救了。

接下来,我想根据任务变量、阶段(其值为 Planning、Construction 或 Final)过滤任务列表。因此,我创建了 4 个按钮(每个阶段一个,然后是“所有任务”以返回完整的、未过滤的列表),经过大量的试验和错误(创建不再正确绑定的过滤数组等)。等)我尝试过这个,基本上只使用原始数组。

            List {
                ForEach(project.tasks.filter({ $0.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)
                    }
                }
            }
Run Code Online (Sandbox Code Playgroud)

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

    func CreateTestArray() {
        let testFilterArray = project.tasks.filter({ $0.phase.contains(filterValue) })
    }
Run Code Online (Sandbox Code Playgroud)

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

例如,我有 128 个任务,其中 10 个任务的值为“Final”,当我使用按钮将 filterValue 设置为 Final 时,testFilterArray 实际上包含正确的 10 个任务 - 但在 ForEach 视图中我得到的是第一个任务原始数组中的十个任务(其类型为“规划” - 原始数组按规划/施工/最终排序);显然,尽管有过滤器语句,ForEach 仍在原始数组上工作。Planning 按钮发送 filterValue =“Planning”,我得到了正确的结果,因为过滤器为原始数组中的 20 个规划任务返回 0-19 个索引,并且由于它们位于原始数组中的第一个,因此“看起来“规划过滤器工作正常”,但实际上它只是偶然工作,如果数组以不同的方式排序,它就不会工作。

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

谢谢!

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

我觉得你的答案为我指明了正确的决定,我只需要备份并查看那些对象定义/管理,看看如何达到可以提供简单解决方案的情况。

任务:

 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
    }
}
Run Code Online (Sandbox Code Playgroud)

该项目:

 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
    }

}
Run Code Online (Sandbox Code Playgroud)

和项目模型:

 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
Run Code Online (Sandbox Code Playgroud)

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

在我看来:

 List {
  ForEach(project.tasks.filter({ $0.phase.contains(filterValue) })) { task in
    HStack {
      Text(task.name)
      Toggle("", isOn: self.makeBinding(item: task))
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

以及被调用的函数:

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

jn_*_*pdx 3

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

ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
Run Code Online (Sandbox Code Playgroud)

在第一部分中,您进行过滤tasks,然后询问索引。我怀疑您希望它返回类似 [1, 5, 10, 11, 12] 的内容,这意味着它们在数组中的原始位置。但是,实际上,您将获得一个像 [0,1,2,3,4] 这样的连续数组,因为它为您提供新创建的数组的索引( 的结果filter)。

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

ForEach迭代结构/对象比索引更惯用。你没有显示它Task是由什么组成的,但我们假设它是这样的:

struct Task : Hashable {
  var id = UUID()
  var name: String
  var phrase: String
  var isComplete: Bool
}
Run Code Online (Sandbox Code Playgroud)

要迭代它,你可以这样做:

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

我在评论中询问了 的类型,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 { $0.isComplete }
    }
    
    func taskCompletedBinding(id: UUID) -> Binding<Bool> {
        Binding<Bool>(get: {
            self.tasks.first(where: { $0.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
                }
            }
        })
    }
}

Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式测试它是否有效:

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))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果Project是一个结构体而不是一个对象,那么将它包装在视图模型中可能会很好,ObservableObject就像我上面所做的那样。