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

发布于 2025-01-12 16:32:25 字数 3684 浏览 0 评论 0原文

我有两个嵌套的 ForEach 语句,它们一起创建一个带有节标题的列表。节标题是从列表中项目的属性中收集的。 (在下面的示例中,节标题是类别。)

我希望用户能够从列表下方的数组中删除项目。这里的复杂性在于 .onDelete() 返回节内行的索引,因此为了正确识别要删除的项目,我还需要使用节/类别,如 此 StackOverflow 帖子。不过,这篇文章中的代码对我不起作用 - 由于某种原因,类别/区域变量不可在 onDelete() 命令中使用。我确实尝试在迭代第一个 ForEach 的类别时将类别转换为索引(即 ForEach(0..< appData.sortedByCategories.count) { i in ),但这并没有'也不工作。另一个问题是,我理想情况下希望使用 appData.deleteItem 函数来执行删除(见下文),但我无法从 StackOverflow 帖子中获取有关此问题的代码来工作即使我没有这样做。

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

@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: {$0.category})
    }
    
    // Sort the category-based dictionary alphabetically
    var sortedByCategories: [String] {
        dictGroupedByCategory.map({ $0.key }).sorted(by: {$0 < $1})
    }
    
    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: {$0.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

I have two nested ForEach statements that together create a list with section headers. The section headers are collected from a property of the items in the list. (In my example below, the section headers are the categories.)

I'm looking to enable the user to delete an item from the array underlying the list. The complexity here is that .onDelete() returns the index of the row within the section, so to correctly identify the item to delete, I also need to use the section/category, as explained in this StackOverflow posting. The code in this posting isn't working for me, though - for some reason, the category/territorie variable is not available for use in the onDelete() command. I did try converting the category to an index when iterating through the categories for the first ForEach (i.e., ForEach(0..< appData.sortedByCategories.count) { i in ), and that didn't work either. An added wrinkle is that I'd ideally like to use the appData.deleteItem function to perform the deletion (see below), but I couldn't get the code from the StackOverflow post on this issue to work even when I wasn't doing that.

What am I overlooking? The example below illustrates the problem. Thank you so much for your time and any insight you can provide!

@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: {$0.category})
    }
    
    // Sort the category-based dictionary alphabetically
    var sortedByCategories: [String] {
        dictGroupedByCategory.map({ $0.key }).sorted(by: {$0 < $1})
    }
    
    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: {$0.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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

怀念你的温柔 2025-01-19 16:32:25

我已经找到了上述问题的解决方案,并将其发布在这里,以防其他人遇到同样的问题。该解决方案涉及使用 .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: {$0.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

I've figured out a solution to my question above, and am posting it here in case anyone else ever struggles with this same issue. The solution involved using .swipeActions() rather than .onDelete(). For reasons I don't understand, I could attach .swipeActions() (but not .onDelete()) to the Text(item.name) line of code. This made the "item" for each ForEach iteration available to the .swipeAction code, and everything else was very straightforward. The revised ContentView code now looks like this:

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: {$0.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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文