SwiftUI — onDelete

在 SwiftUI 中,onDelete 通常与列表(List)结合使用,用来支持从数据源中移除项的功能。正确的使用层级是在列表的子视图中绑定 ForEach 或其他类型的数据管理容器,确保操作仅适用于具体的数据项,同时也能准确执行删除操作。

要回答 “onDelete 应该放在哪一个层级” 的问题,我们需要理解 onDelete 的作用及其最佳实践。


1. 什么是 onDelete? #

onDelete(perform:) 是 SwiftUI 提供的一种方法,用于在列表中实现“滑动删除”功能。它会通过一个 回调闭包 给出被滑动删除的索引,您需要根据这个索引从数组中移除被对应的数据。

用法特性: #

  1. 绑定在 ForEach 的数据容器上

    • ForEach 是 SwiftUI 的一种描述性循环结构生产多个子视图,onDelete 作用于这个视图集。
    • 需要在 ForEach 的数据层级中移除对应索引的数据项。
  2. 提供删除行为能力

    • 您需要实现一个闭包,该闭包接收删除操作产生的 IndexSet,表示被删除项的位置集合。
  3. 在绑定的数据源中删除数据项

    • 删除与列表相关的数据(如数组中的项),并确保列表能够正确刷新。

2. onDelete 应该放在哪个层级? #

2.1 放在 ForEach 内的层级 #

onDelete 应该直接绑定到用于生成视图列表的 ForEach。原因是:

  • ForEach 负责迭代数据源,并生成对应的每个子视图。
  • onDelete 的操作基于 IndexSet,而 IndexSet 恰好映射到 ForEach 的数据绑定。

示例代码:正确使用层级 #

以下是一个正确的示例,展示如何使用 onDelete

struct ContentView: View {
    @State private var items = ["Item 1", "Item 2", "Item 3"]

    var body: some View {
        List {
            // ForEach 直接绑定到数据源
            ForEach(items, id: \.self) { item in
                Text(item)
            }
            // 将删除功能绑定到 ForEach
            .onDelete(perform: deleteItems)
        }
    }

    func deleteItems(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
}

解释: #

  1. 这里的 ForEach(items, id: \.self) 绑定到数组 items,生成列表内容。
  2. ForEach 上调用 .onDelete(perform:),确保删除操作绑定到它直接管理的 items 数据源。
  3. 实现 deleteItems(at:) 方法,用索引集 (IndexSet) 更新数组,完成删除操作。

3. 错误的层级:放在 List 外层级 #

如果将 onDelete 放在 List 外部或更高层级(例如不绑定 ForEach),会导致以下问题:

示例:错误用法 #

struct ContentView: View {
    @State private var items = ["Item 1", "Item 2", "Item 3"]

    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                Text(item)
            }
        }
        .onDelete(perform: deleteItems) // ❌ 错误,.onDelete 应绑定 ForEach
    }

    func deleteItems(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
}

错误原因: #

  • .onDelete 必须与 ForEach 配合使用,而不是直接绑定到 List
  • List 是父容器,实际上它并不了解 ForEach 的数据源,无法将删除操作作用到适当的索引。

4. onDelete 的高级用法与动态绑定 #

4.1 可编辑列表 #

在实际应用中,您可以实现更复杂的逻辑,比如支持删除多个对象,或者检查删除条件:

代码示例:动态展示不同列表内容 #

struct ContentView: View {
    @State private var fruits = ["Apple", "Orange", "Banana"]
    @State private var vegetables = ["Carrot", "Broccoli"]

    var body: some View {
        List {
            Section(header: Text("Fruits")) {
                ForEach(fruits, id: \.self) { fruit in
                    Text(fruit)
                }
                .onDelete { indexSet in
                    fruits.remove(atOffsets: indexSet)
                }
            }

            Section(header: Text("Vegetables")) {
                ForEach(vegetables, id: \.self) { vegetable in
                    Text(vegetable)
                }
                .onDelete { indexSet in
                    vegetables.remove(atOffsets: indexSet)
                }
            }
        }
        .listStyle(InsetGroupedListStyle())
    }
}

动态绑定解释#

  1. 多个 ForEach 数据源展示在不同的 Section 中,每个 Section 管理独立的数据源。
  2. onDelete 动态绑定到 { fruits.remove }{ vegetables.remove },确保只修改各自对应的数据源。

4.2 可选删除 #

如果删除列表中的某些项需要逻辑校验,可以在 deleteItems(at:) 中添加校验逻辑:

func deleteItems(at offsets: IndexSet) {
    for index in offsets {
        if shouldAllowDelete(for: items[index]) {
            items.remove(at: index)
        } else {
            print("Cannot delete item at \(index)")
        }
    }
}

func shouldAllowDelete(for item: String) -> Bool {
    return item != "Special Item"
}

5. 总结:确定 onDelete 的层级 #

  • 绑定位置: onDelete 应直接放在 ForEach 的层级上,而不是 List 或其他容器的外层。
  • 如何工作: 它依赖于 ForEach 的数据源,从提供的 IndexSet 中计算删除操作。
  • 使用建议:
    1. 确保 ForEach 和数据源直接绑定。
    2. 在删除逻辑中动态处理索引,避免越界或操作错误。
    3. 对于多组数据,分别在每个 ForEach 上绑定独立的删除逻辑。

正确的常见示例模板 #

List {
    Section(header: Text("Group 1")) {
        ForEach(itemsGroup1, id: \.id) { item in
            Text(item.name)
        }
        .onDelete(perform: deleteGroup1Items)
    }

    Section(header: Text("Group 2")) {
        ForEach(itemsGroup2, id: \.id) { item in
            Text(item.name)
        }
        .onDelete(perform: deleteGroup2Items)
    }
}
本文共 1335 字,上次修改于 Jan 4, 2025