1

I have a simple list populated using a view-model:

@ObservedObject var sdvm = StepDataViewModel()

[...]

List {
    ForEach (vm.steps.indices, id: \.self) { idx in
        TheSlider(value: self.$vm.steps[idx].theValue, index: self.vm.steps[idx].theIndex)
    }
    .onDelete(perform: { indexSet in
        self.vm.removeStep(index: indexSet) // <--- here
    })
}

Where the viewmodel is this:

class StepDataViewModel: ObservableObject {
    @Published var steps: [StepData] = []

    func removeStep(index: IndexSet) {
        steps.remove(atOffsets: index)
    }    
}

and the StepData is this one:

struct StepData: Equatable, Hashable {
    var theIndex: Int
    var theValue: Double
}

TheSlider:

struct TheSlider: View {
    @Binding var value: Double
    @State   var index: Int

    var body: some View {
        ZStack {
            Slider(value: $value, in: 0...180, step: 1)
                .padding()
            HStack {
                Text("[\(index)]")
                    .font(.body)
                    .fontWeight(.black)
                    .offset(y: -20.0)

                Text("\(Int(value))")
                    .offset(y: -20.0)
            }
        }
    }
}

Now, .onDelete is obviously attached to the List, so I receive the correct row index when press delete.
For what reason the app crash for index-out-of-bound? Is the list that pass me the index, or not?

I receive this error:

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444

Can be caused by the direct array reference in the "TheSlider"? If yes, how I can change it using theValue as Binding updatable?

elp
  • 8,021
  • 7
  • 61
  • 120

2 Answers2

1

This is a SwiftUI bug reported in Deleting list elements from SwiftUI's List.

The solution is to use the extension from here that prevents accessing invalid bindings:

struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
   
   typealias BoundElement = Binding<T.Element>
   private let binding: BoundElement
   private let content: (BoundElement) -> C

   init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) {
      self.content = content
      self.binding = .init(get: { binding.wrappedValue[index] }, 
                           set: { binding.wrappedValue[index] = $0 })
   }
   
   var body: some View { 
      content(binding)
   }
}
List {
    ForEach(vm.steps.indices, id: \.self) { index in
        Safe(self.$vm.steps, index: index) { binding in
            TheSlider(value: binding.theValue, index: vm.steps[index].theIndex)
        }
    }
    .onDelete(perform: { indexSet in
        self.vm.removeStep(index: indexSet)
    })
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • I have no idea on how you reached this "Safe" solution but it works well. I need to study a lot. Thanks! – elp Mar 19 '21 at 21:05
0

I believe this is the same problem addressed here which is a loss of consistency between the indices in the ForEach loop and what is deleted. Add a separate array for the indices and use it for iteration and deletion like this:

    @ObservedObject var vm = StepDataViewModel()
    @State var indices: [Int] = Array(0..<3)
    
    var body: some View {
        
        List {
            ForEach (indices, id: \.self) { idx in
                TheSlider(value: self.$vm.steps[idx].theValue, index: self.vm.steps[idx].theIndex)
            }
            .onDelete(perform: { indexSet in
                indices.remove(atOffsets: indexSet) // <--- here
            })
        }

You will need some additional code to handle deletion of the corresponding items in the model

Bill Aylward
  • 31
  • 1
  • 1