在 SwiftUI 中,如何删除嵌套在两个 ForEach 语句中的列表行?

In SwiftUI, how do you delete a list row that is nested within two ForEach statements?

我有两个嵌套的 ForEach 语句,它们一起创建了一个包含 headers 部分的列表。 headers 部分是从列表中的 属性 项中收集的。 (在我下面的示例中,headers 部分是类别。)

我希望让用户能够从列表下方的数组中删除一个项目。这里的复杂性在于 .onDelete() returns 部分 行的索引 ,因此要正确识别要删除的项目,我还需要使用 section/category,如 中所述。不过,此 posting 中的代码对我不起作用 - 出于某种原因,category/territorie 变量不可用于 onDelete() 命令。在遍历第一个 ForEach 的类别(即 ForEach(0..< appData.sortedByCategories.count) { i in )时,我确实尝试将类别转换为索引,但这也没有用。一个额外的问题是,我理想情况下希望使用 appData.deleteItem 函数来执行删除(见下文),但我无法从 Whosebug post 获得有关此问题的代码甚至可以正常工作当我不这样做的时候。

我忽略了什么?下面的例子说明了这个问题。非常感谢您抽出宝贵时间和提供任何见解!

@main
struct NestedForEachDeleteApp: App {
    
    var appData = AppData()
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(appData)
        }
    }
}

import Foundation
import SwiftUI

class AppData: ObservableObject {
    
    static let appData = AppData()
    @Published var items = [Item]()
    
    // Create a dictionary based upon the category
    var dictGroupedByCategory: [String: [Item]] {
        Dictionary(grouping: items.sorted(), by: {[=10=].category})
    }
    
    // Sort the category-based dictionary alphabetically
    var sortedByCategories: [String] {
        dictGroupedByCategory.map({ [=10=].key }).sorted(by: {[=10=] < })
    }
    
    func deleteItem(index: Int) {
        items.remove(at: index)
    }
    
    init() {
        items = [
            Item(id: UUID(), name: "Item 1", category: "Category A"),
            Item(id: UUID(), name: "Item 2", category: "Category A"),
            Item(id: UUID(), name: "Item 3", category: "Category B"),
            Item(id: UUID(), name: "Item 4", category: "Category B"),
            Item(id: UUID(), name: "Item 5", category: "Category C"),
        ]
    } // End of init()
    
}

class Item: ObservableObject, Identifiable, Comparable, Equatable {
    var id = UUID()
    @Published var name: String
    @Published var category: String
    
    // Implement Comparable and Equable conformance
    static func <(lhs: Item, rhs: Item) -> Bool {
        return lhs.name < rhs.name
    }
    
    static func == (lhs: Item, rhs: Item) -> Bool {
        return lhs.category < rhs.category
    }
    
    // MARK: - Initialize
    init(id: UUID, name: String, category: String) {
        
        // Initialize stored properties.
        self.id = id
        self.name = name
        self.category = category
        
    }
    
}

struct ContentView: View {
    
    @EnvironmentObject var appData: AppData
    
    var body: some View {
        List {
            ForEach(appData.sortedByCategories, id: \.self) { category in
                Section(header: Text(category)) {
                    ForEach(appData.dictGroupedByCategory[category] ?? []) { item in
                        Text(item.name)
                    } // End of inner ForEach (items within category)
                    .onDelete(perform: self.deleteItem)
                } // End of Section
            } // End of outer ForEach (for categories themselves)
        } // End of List
    } // End of body View
    
    func deleteItem(at offsets: IndexSet) {
        for offset in offsets {
            print(offset)
        //  print(category) // error = "Cannot find 'category' in scope
        //  let indexToDelete = appData.items.firstIndex(where: {[=10=].id == item.id }) // Error = "Cannot find 'item' in scope
            
            appData.deleteItem(index: offset) // this code will run but it removes the wrong item because the offset value is the offset *within the category*
        }
    }
    
} // End of ContentView

我已经找到了解决上述问题的方法,并将其张贴在这里以防其他人遇到同样的问题。解决方案涉及使用 .swipeActions() 而不是 .onDelete()。由于我不明白的原因,我可以将 .swipeActions()(但不是 .onDelete())附加到 Text(item.name) 代码行。这使得每次 ForEach 迭代的“项目”可用于 .swipeAction 代码,其他一切都非常简单。修改后的 ContentView 代码现在如下所示:

struct ContentView: View {
    
    @EnvironmentObject var appData: AppData
    
    var body: some View {
        List {
            ForEach(appData.sortedByCategories, id: \.self) { category in
                Section(header: Text(category)) {
                    ForEach(appData.dictGroupedByCategory[category] ?? []) { item in
                        Text(item.name)
                            .swipeActions(allowsFullSwipe: false) {
                                Button(role: .destructive) {
                                    print(category)
                                    print(item.name)
                                    
                                    if let indexToDelete = appData.items.firstIndex(where: {[=10=].id == item.id }) {
                                        appData.deleteItem(index: indexToDelete)
                                    } // End of action to perform if there is an indexToDelete
                                    
                                } label: {
                                    Label("Delete", systemImage: "trash.fill")
                                }
                            } // End of .swipeActions()
                    } // End of inner ForEach (items within category)
                } // End of Section
            } // End of outer ForEach (for categories themselves)
        } // End of List
    } // End of body View
} // End of ContentView