Deleting List Item With A Binding Boolean Value - SwiftUI

358 views Asked by At

In my app, I have a screen where the user can switch filters (of a map) on and off using the Swift Toggle, and also, delete them (with Edit Button). I use a Binding Boolean variable to connect the toggles so that I can react to each change, here is the code:

//The filter data model
class DSFilter{
    
    var filterId : Int
    var filterTitle : String
    var isActive : Bool
    
    
    init(filterId : Int, filterTitle : String, isActive : Bool){
        self.filterId = filterId
        self.filterTitle = filterTitle
        self.isActive = isActive
    }
}

//The data model representable
struct filterItem : View {
    @Binding var filter : DSFilter
    @Binding var isOn : Bool
    @State var image = Image("defPerson")
    
    var body : some View {
        HStack {
            image.resizable().frame(width: 45, height: 45).clipShape(Circle())
            VStack(alignment: .leading, spacing: 8) {
                Text(filter.filterTitle).font(Font.custom("Quicksand-Bold",size: 15))
                Text(filter.filterTitle).font(Font.custom("Quicksand-Medium",size: 13)).foregroundColor(.gray)
            }.padding(.leading)
            Spacer()
            HStack{
                Toggle("",isOn: $isOn).padding(.trailing, 5).onReceive([self.isOn].publisher.first()) { (value) in
                    self.filter.isActive = self.isOn
                }.frame(width: 30)
            }
        }.frame(height: 60)
            .onAppear(){
                self.isOn = self.filter.isActive             
        }
    }
}

//The view where the user has the control
struct FilterView: View {
    @State var otherFilters : [addedFilter] = []
    @Binding var isActive : Bool
    
    var body: some View {
        VStack{
            Capsule().fill(Color.black).frame(width: 50, height: 5).padding()
            ZStack{
                HStack{
                    Spacer()

                    Text("Filters").font(Font.custom("Quicksand-Bold", size: 20))

                    Spacer()
                }.padding()
                HStack{
                    Spacer()
                    
                    EditButton()
                    
                }.padding()
            }

                List{
                    
                    ForEach(0..<self.otherFilters.count, id:\.self){ i in
                        
                        filterItem(filter: self.$otherFilters[i].filter, isOn: self.$otherFilters[i].isOn)
                    }.onDelete(perform:removeRows)
                   
   
                
            }
            Spacer()
        }
        
    }
    
    func removeRows(at offsets: IndexSet) {
        print("deleting")
                    print("deleted filter!")
            self.otherFilters.remove(at: Array(offsets)[0]) //I use it like so because you can only delete 1 at a time anyway
       
        
    }
}

//The class I use to bind the isOn value so I can react to change
struct addedFilter : Identifiable{
    var id : Int
    var filter : DSFilter
    var isOn : Bool
    
    init(filter : DSFilter){
        self.id = Int.random(in: 0...100000)
        self.filter = filter
        self.isOn = filter.isActive
    }
}

When I use the delete as it is right now I get the following error (Xcode 12.0):

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
2020-10-06 17:42:12.567235+0300 Hynt[5207:528572] Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444

What do I need to change so I can delete the items? Should I take a different approach to the Binding problem with the Toggle?

3

There are 3 answers

0
TheMachineX On BEST ANSWER

So, after a lot of try and fail I came to the answer. The problem seems to be in the binding part - the compiler can't handle a change in the array size if I bind an element from it so I did the following: First, I change the filterItem struct so the filter property is @State and not @Binding :

struct filterItem : View {
    @State var filter : DSFilter
    @Binding var isOn : Bool

Secondly, I have changed the addedFilter struct to the following:

struct addedFilter : Identifiable{
    var id : Int
    var isOn : Bool
    
    init(filter : DSFilter){
        self.id = filter.filterId
        self.isOn = filter.isActive
    }
}

And the ForEach loop to the following:

ForEach(0..<self.normOthFilters.count, id:\.self){ i in
                        filterItem(filter: self.normOthFilters[i], isOn: self.$otherFilters[i].isOn)
                    }.onDelete(perform:removeRows)

To the main view I have added :

    @State var normDefFilters : [DSFilter] = []
    @State var normOthFilters : [DSFilter] = []

And finally the onDelete function to the following:

func removeRows(at offsets: IndexSet) {
        print("deleting")
            print("deleted filter!")
            self.normOthFilters.remove(atOffsets: offsets)
        
    }

In conclution I am not changing the value of the binded value and so I am not receiving any errors!

0
Gabriel Balta On

I suggest you to change your line in removeRows() function from

self.otherFilters.remove(at: Array(offsets)[0]

to

self.otherFilters.remove(atOffsets: offsets) 
3
iTag On

Hi instead of keeping three different array to manage addedFilter, DSFilter, isOn. why don't you try the following one single model

class FFilter: Identifiable {
var id: Int
 var addedFilter: addedFilter
var dsFilter: DSFilter
var isOn: Bool

init(addedFilter: addedFilter, dsFilter: DSFilter, isOn: Bool) {
    self.id = addedFilter.id
    self.addedFilter = addedFilter
    self.dsFilter = dsFilter
    self.isOn = isOn
}

}

And ViewModel change code like this:

class ViewModel: ObservableObject {

@Published var superFilters: [FFilter] = [
    FFilter(
        addedFilter: addedFilter(
            filter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true)
        ),
        dsFilter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true),
        isOn: true),
    FFilter(
        addedFilter: addedFilter(
            filter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false)
        ),
        dsFilter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false),
        isOn: false),
    FFilter(
        addedFilter: addedFilter(filter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true)),
        dsFilter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true),
        isOn: true),
    FFilter(
        addedFilter: addedFilter(
            filter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true)
        ),
        dsFilter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true),
        isOn: true),
    FFilter(
        addedFilter: addedFilter(
            filter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false)
        ),
        dsFilter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false),
        isOn: false)
]}

then change your list code

  List {
 ForEach(viewModel.superFilters) { filter in
      filterItem(filter: .constant(filter.dsFilter), isOn: .constant(filter.isOn))
      }.onDelete(perform: removeRows(at:)) }

and removeRows method:

func removeRows(at offsets: IndexSet) {
    print("deleting at \(offsets)")
    self.viewModel.superFilters.remove(atOffsets: offsets)}

I hope it will work. let me know if you still face any issues