删除ForEach循环中的项目会导致致命错误:索引超出范围

时间:2020-06-24 23:56:18

标签: swiftui

我有一个ScrollView,它使用数组中的ForEach循环显示行列表。当我从数组中删除一项时,出现错误:索引超出范围。

ScrollView {
    ForEach(viewModel.tasks.indices, id: \.self){ index in
        TaskRow(
            task: self.$viewModel.tasks[index],
            deleteAction: {
                self.viewModel.deleteTask(task: self.viewModel.tasks[index])
            }
        )
    }
}

此错误仅在我切换为从ForEach循环而不是“任务”本身传递索引时才开始发生。我必须这样做,以便可以在子视图“ TaskRow”中使用@Binding var task: Task

“删除操作”由子视图中的按钮触发。

viewModel.deleteTask的工作方式如下(使用dataManager):

final class StackDetailViewModel: ObservableObject {
    
    @Published var tasks = [Task]()
    
    var dataManager: DataManagerProtocol
    
    init(dataManager: DataManagerProtocol = DataManager.shared){
        self.dataManager = dataManager
        fetchTasks()
    }
}
extension StackDetailViewModel {

    func fetchTasks() {
        tasks = dataManager.fetchTasks()
    }

    func deleteTask(task: Task) {
        dataManager.deleteTask(task: task)
        fetchTasks()
    }

}

dataManager在何处执行


Class DataManager {

...

    private var tasks = [Task]()

...
 
    func fetchTasks() -> [Task] {
        tasks
    }

    func deleteTask(task: Task) {
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks.remove(at: index)
        }
    }

}

我在我的应用程序中使用协议,但为简单起见,在此已将其删除。 任何帮助将不胜感激。

1 个答案:

答案 0 :(得分:0)

直接在 ForEach 中使用数组索引作为标识符不是一个好习惯,因为索引是位置性的并且不标识它们所代表的项目。这可能会导致 SwiftUI 中的重绘问题和崩溃。
例如。当您在常规 List 中使用“滑动删除”时,SwiftUI 知道给定位置的项目已被删除,并且可能不会再次询问完整的 ID 列表(在这种不正确的情况下是索引)重绘内容。 SwiftUI 将简单地为下一次重绘提供剩余 ID 的列表,这会导致越界崩溃。

我并不是说这就是这里的确切情况,因为我无法重现您的崩溃。

当您自己编写代码时,当您修改 ForEach 以使用索引而不是您的任务时会出现问题。

代替

ForEach(viewModel.tasks.indices, id: \.self){ index in

使用

ForEach(Array(viewModel.tasks.enumerated()), id: \.1.id) { (index, task) in

这使您可以访问 Binding 所需任务和相关任务的 TaskRow 索引。此外,通过将任务 ID 指定为每一行的标识符,SwiftUI 将不再依赖位置索引。

但要注意:对于非常大的任务数组,此解决方案可能效果不佳,因为 EnumeratedSequence(由 .enumerated() 在您的任务 Array ) 被展平为新的 Array

因此,如果您正在处理大量任务列表,我会推荐您的初始版本:ForEach(viewModel.tasks),最终仅在 deleteAction 的情况下支付性能损失。