Galen Smith
Galen Smith

Reputation: 387

Delete List Entries Containing a Button?

I have an accounting app with a screen that displays two views. One view displays Currently In Use Categories and another that displays Available Categories. The user may select which categories to use and remove other not needed. Tapping on a category in either box immediately moves the category to the other category box. These programmed categories may not be deleted.

In addition, the user may create their own optional categories. The trouble I'm having is deleting the optional categories. (The Add New Category works fine.) I know how to normally implement view delete using List and ForEach but this seems somewhat different. If I add onDelete at the bottom of the ForEach loop and change the VStack to a List I don't see any entries in the list. Changing the List to a VStack displays the categories in the list but doesn't let me delete any categories. Another possible problem is dragging a category to the left for deletion may also trigger the switch view button.

I am filtering the data because I was getting gaps in the lists for categories currently in the other view.

The code below is the logic for displaying the Currently In Use box.

struct GetCurrentCats: View {

    var g: GeometryProxy

    @State private var showStatus: Bool = false

    @Binding var presentAlert: Bool
    @Binding var presentDetail: String

    @EnvironmentObject var categories: Categories

    var body: some View {
      
        //VStack (alignment: .leading, spacing: 6) {
        List {
            ForEach(categories.filteredInUse, id: \.id) { item in

                Button(action: {
                    if item.catTotal == 0.0 { // must have a zero balance to remove

                        withAnimation {
                            showStatus = false

                            // Change the state of category
                            categories.setCatShow(forId: item.id)
                        }
                    } else {
                        presentAlert = true
                        presentDetail = item.catName
                    }
                }){
                    HStack {
                        Image(systemName: item.catPix)
                            .resizable()
                            .foregroundColor(Color(.systemOrange))
                            .frame(width: 25, height: 25)
                            .frame(width: UIDevice.current.userInterfaceIdiom == .phone ? g.size.width * 0.40 : g.size.width * 0.20)

                        Text(item.catName)
                            .padding(.horizontal, 8)
                            .background(item.catTotal != 0.0 ? Color(.systemRed) : Color(.systemBlue))
                            .foregroundColor(Color.white)
                            .cornerRadius(8)
                    }
                }
            }.onDelete(perform: deleteCurCat)
        }.frame(maxWidth: .infinity, alignment: .leading)
        }
    }
    func deleteCurCat(indexSet: IndexSet) {
        categories.catItem.remove(atOffsets: indexSet)
    }
}

Here is class Categories:

// working categories
struct CatModel:  Codable, Identifiable, Hashable {
    var id = UUID()
    var catName: String     // category name
    var catTotal: Double    // category total
    var catBudget: Double   // category budget
    var catPix: String      // sf symbol
    var catShow: Bool

    /// Provides a new instance, toggling `catShow`.
        var toggleCatShow: CatModel {

            // Note: by creating a new instance, the new id will be different
            CatModel(catName: catName, catTotal: catTotal, catBudget: catBudget, catPix: catPix, catShow: !catShow)
    }
}


class Categories: ObservableObject {

    @Published var filteredInUse: [CatModel] = []
    @Published var filteredNotInUse: [CatModel] = []

    @Published var catItem: [CatModel] {
        didSet {
            self.save()
         
        }
    }

    init() {

        // read in category data
        if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
            if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
                self.catItem = decoded
                self.updateStatus()
                return
            }
        }

        catItem = []

        // catShow: 1 = category available for use; 2 = category currently being used
        let item0 = CatModel(catName: "Lodging", catTotal: 0.0, catBudget: 0, catPix: "bed.double.fill", catShow: false)
        self.catItem.append(item0)

        let item1 = CatModel(catName: "Food", catTotal: 0.0, catBudget: 0, catPix: "cart", catShow: false)
        self.catItem.append(item1)

        let item2 = CatModel(catName: "Airplane", catTotal: 0.0, catBudget: 0, catPix: "airplane", catShow: false)
        self.catItem.append(item2)

        let item3 = CatModel(catName: "Train", catTotal: 0.0, catBudget: 0, catPix: "tram", catShow: false)
        self.catItem.append(item3)

        let item4 = CatModel(catName: "Bus", catTotal: 0.0, catBudget: 0, catPix: "bus.fill", catShow: false)
        self.catItem.append(item4)

        let item5 = CatModel(catName: "Ferry", catTotal: 0.0, catBudget: 0, catPix: "ferry", catShow: false)  // "helm"
        self.catItem.append(item5)

        let item6 = CatModel(catName: "Local Transit", catTotal: 0.0, catBudget: 0, catPix: "textbox", catShow: false)
        self.catItem.append(item6)

        let item7 = CatModel(catName: "Sightseeing  ", catTotal: 0.0, catBudget: 0, catPix: "photo", catShow: false)
        self.catItem.append(item7)

        let item8 = CatModel(catName: "Entertainment", catTotal: 0.0, catBudget: 0, catPix: "music.mic", catShow: false)
        self.catItem.append(item8)

        let item9 = CatModel(catName: "Souvenirs", catTotal: 0.0, catBudget: 0, catPix: "gift", catShow: false)
        self.catItem.append(item9)

        let item10 = CatModel(catName: "Laundry", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: false)
        self.catItem.append(item10)

        let item11 = CatModel(catName: "Rental Car", catTotal: 0.0, catBudget: 0, catPix: "car", catShow: false)
        self.catItem.append(item11)

        let item12 = CatModel(catName: "Fuel", catTotal: 0.0, catBudget: 0, catPix: "fuelpump", catShow: false)   // gauge
        self.catItem.append(item12)

        let item13 = CatModel(catName: "Parking", catTotal: 0.0, catBudget: 0, catPix: "parkingsign.circle", catShow: false)
        self.catItem.append(item13)

        self.updateStatus()
    }

    func updateStatus() {
        filteredInUse = catItem.filter({ (user) -> Bool in
            return user.catShow == true
        })

        filteredNotInUse = catItem.filter({ (user) -> Bool in
            return user.catShow == false
        })
    }



    // Replaces an instance of `CatModel` by changing its `catShow` value.
       func setCatShow(forId id: UUID) {
           let index = catItem.firstIndex { $0.id == id }
           if let index = index {
               catItem.replaceSubrange(index...index, with: [catItem[index].toggleCatShow])
           }

           // Refresh the filtered arrays
           updateStatus()
       }

        add new optional category (8 max)
    func addNewCatetory(catName: String) -> () {

        let item = CatModel(catName: catName, catTotal: 0.0, catBudget: 0, catPix: "person.2", catShow: true)
        catItem.append(item)
    }
}

Here is the upper piece of my Update Categories logic. If I run the example code with GetCurrentCats(), it displays the programmed categories. If I include this upper portion of the logic that sets up the two boxes and text above and below each box then GetCurrentCats doesn't display any programmed categories but GetAvailableCats() does display its categories. It appears to be something with the onDelete in GetCurrentCats that it doesn't like.

struct ContentView: View {

    @EnvironmentObject var categories: Categories

    @State private var presentAlert = false
    @State private var presentDetail: String = ""

    var body: some View {

        GeometryReader { g in

            VStack (alignment: .center) {

                Text("Current Categories")
                    .font(.headline)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding(.top, 6)
                    .padding(.bottom, -3)

                ScrollView {
                    GetCurrentCats(g: g, presentAlert: $presentAlert, presentDetail: $presentDetail)

                } .frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
                    .border(Color(.systemBlue), width: 4)

                Text("Tap category to remove from system.")
                    .font(.footnote)
                Text("Red categories have a nonzero balance.")
                    .font(.footnote)
                Text("Categories must have a zero balance to remove.")
                    .font(.footnote)
                    .padding(.bottom, g.size.height * 0.04)

                Text("Available Categories")
                    .font(.headline)
                    .padding(.top, g.size.height * 0.05)
                    .padding(.bottom, -3)

                ScrollView {
                    GetAvailableCats(g: g)

                } .frame(width: g.size.width * 0.80, height: g.size.height * 0.35).border(Color(.systemBlue), width: 4)

                Text("Tap category to add to system.")
                    .font(.footnote)

            }.alert(
                "Unable to remove category.", isPresented: $presentAlert,
                presenting: presentDetail
            ) { detail in

                Button("OK") {}
            } message: { detail in
                Text("\(presentDetail) category has a nonzero balance.")
            }
            .navigationBarTitle("Setup Categories", displayMode: .inline)
            .navigationViewStyle(StackNavigationViewStyle())
        /*    .navigationBarItems(trailing: NavigationLink (destination: AddCatView()) {
                Image(systemName: "plus")
                    .resizable()
                    .frame(width: 18, height: 18)
            })  */
        }
    }
}

Upvotes: 0

Views: 176

Answers (1)

you should not be using duplicate, triplicate arrays to store CatModels of different catShow values. Use only one array catItem and filter the array according to your needs. Such as: filteredInUse equivalent, use categories.catItem.filter{ $0.catShow.wrappedValue } and filteredNotInUse equivalent, use categories.catItem.filter{ !$0.catShow.wrappedValue } in the ForEach loop.

I've updated your code using this approach.

// working categories
struct CatModel:  Codable, Identifiable, Hashable {
    var id = UUID()
    var catName: String     // category name
    var catTotal: Double    // category total
    var catBudget: Double   // category budget
    var catPix: String      // sf symbol
    var catShow: Bool
}

class Categories: ObservableObject {
    // the array of CatModel
    @Published var catItem: [CatModel] {
        didSet {
            //   self.save()
        }
    }
    
    init() {
        
        // you should not use this to store the array of CatModels.
        // UserDefaults is meant to be used only for small amount of data.
        
        // read in category data
//        if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
//            if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
//                self.catItem = decoded
//                return
//            }
//        }
        
        catItem = []
        
        let item0 = CatModel(catName: "Lodging", catTotal: 0.0, catBudget: 0, catPix: "bed.double.fill", catShow: true)
        self.catItem.append(item0)
        
        let item1 = CatModel(catName: "Food", catTotal: 0.0, catBudget: 0, catPix: "cart", catShow: false)
        self.catItem.append(item1)
        
        let item2 = CatModel(catName: "Airplane", catTotal: 0.0, catBudget: 0, catPix: "airplane", catShow: true)
        self.catItem.append(item2)
        
        let item3 = CatModel(catName: "Train", catTotal: 0.0, catBudget: 0, catPix: "tram", catShow: false)
        self.catItem.append(item3)
        
        let item4 = CatModel(catName: "Bus", catTotal: 0.0, catBudget: 0, catPix: "bus.fill", catShow: true)
        self.catItem.append(item4)
        
        let item5 = CatModel(catName: "Ferry", catTotal: 0.0, catBudget: 0, catPix: "ferry", catShow: false)  // "helm"
        self.catItem.append(item5)
        
        let item6 = CatModel(catName: "Local Transit", catTotal: 0.0, catBudget: 0, catPix: "textbox", catShow: true)
        self.catItem.append(item6)
        
        let item7 = CatModel(catName: "Sightseeing  ", catTotal: 0.0, catBudget: 0, catPix: "photo", catShow: false)
        self.catItem.append(item7)
        
        let item8 = CatModel(catName: "Entertainment", catTotal: 0.0, catBudget: 0, catPix: "music.mic", catShow: true)
        self.catItem.append(item8)
        
        let item9 = CatModel(catName: "Souvenirs", catTotal: 0.0, catBudget: 0, catPix: "gift", catShow: false)
        self.catItem.append(item9)
        
        let item10 = CatModel(catName: "Laundry", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: true)
        self.catItem.append(item10)
        
        let item11 = CatModel(catName: "Rental Car", catTotal: 0.0, catBudget: 0, catPix: "car", catShow: false)
        self.catItem.append(item11)
        
        let item12 = CatModel(catName: "Fuel", catTotal: 0.0, catBudget: 0, catPix: "fuelpump", catShow: true)   // gauge
        self.catItem.append(item12)
        
        let item13 = CatModel(catName: "Parking", catTotal: 0.0, catBudget: 0, catPix: "parkingsign.circle", catShow: false)
        self.catItem.append(item13)
        
    }
    
    // add new optional category (8 max)
    func addNewCatetory(catName: String) {
        catItem.append(CatModel(catName: catName, catTotal: 0.0, catBudget: 0, catPix: "person.2", catShow: true))
    }
}

// for testing
struct ContentView: View {
    @StateObject var categories = Categories()
    @State var presentAlert = false
    @State var presentDetail: String = ""
    
    var body: some View {
        GeometryReader { geom in
            GetCurrentCats(g: geom, presentAlert: $presentAlert, presentDetail: $presentDetail)
                .environmentObject(categories)
        }
    }
}

struct GetCurrentCats: View {
    
    var g: GeometryProxy
    
    @Binding var presentAlert: Bool
    @Binding var presentDetail: String
    
    @EnvironmentObject var categories: Categories
    
    var body: some View {
        
        VStack (alignment: .leading, spacing: 6) {
            List {
                // catShow=true , filteredInUse equivalent list
                ForEach($categories.catItem.filter{ $0.catShow.wrappedValue }, id: \.id) { $item in
                    Button(action: {
                        if item.catTotal == 0.0 { // must have a zero balance to remove
                            withAnimation {
                                // Change the state of category
                                item.catShow = true
                            }
                        } else {
                            presentAlert = true
                            presentDetail = item.catName
                        }
                    }){
                        HStack {
                            Image(systemName: item.catPix)
                                .resizable()
                                .foregroundColor(Color(.systemOrange))
                                .frame(width: 25, height: 25)
                                .frame(width: UIDevice.current.userInterfaceIdiom == .phone ? g.size.width * 0.40 : g.size.width * 0.20)
                            
                            Text(item.catName)
                                .padding(.horizontal, 8)
                                .background(item.catTotal != 0.0 ? Color(.systemRed) : Color(.systemBlue))
                                .foregroundColor(Color.white)
                                .cornerRadius(8)
                        }
                    }
                }.onDelete(perform: deleteCurCat)
            }.frame(maxWidth: .infinity, alignment: .leading)
        }
    }
    
    func deleteCurCat(indexSet: IndexSet) {
        if let index = indexSet.first {
            // you have to filter exactly like in the ForEach
            let cats = categories.catItem.filter({ $0.catShow })
            if index < cats.count {
                if let ndx = categories.catItem.firstIndex(where: { $0.id == cats[index].id }) {
                    //  categories.catItem.remove(at: ndx)  // if you want to totally remove the catItem
                    // to switch/transfer category, just toggle the catShow
                    categories.catItem[ndx].catShow.toggle()
                }
            }
        }
    }

}

EDIT-1: to show the list of categories:

Remove the ScrollView (recommended solution), or set the frame of GetCurrentCats, such as:

ScrollView {
    GetCurrentCats(g: g, presentAlert: $presentAlert, presentDetail: $presentDetail)
        .frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
}
.frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
.border(Color(.systemBlue), width: 4)

Upvotes: 1

Related Questions