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

Time:03-11

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

CodePudding user response:

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
  • Related