Deleting list elements from SwiftUI's List

Using insights from @pawello2222 and @Asperi, I came up with an approach that I think works well, without being overly nasty (still kinda hacky).

I wanted to make the approach more general than just for the simplified example in the question, and also not one that breaks separation of concerns.

So, I created a new wrapper view that creates a binding to an array element inside itself (which seems to fix the state invalidation/update ordering as per @pawello2222's observation), and passes the binding as a parameter to the content closure.

I initially expected to be needing to do safety checks on the index, but turns out it wasn't required for this problem.

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)
   }
}

Usage is:

@ObservedObject var vm: ViewModel = .init()

var body: some View {
   List(vm.arr.indices, id: \.self) { index in
      Safe(self.$vm.arr, index: index) { binding in
         Toggle("", isOn: binding)
         Divider()
         Text(binding.wrappedValue ? "on" : "off")
      }
   }
}

It looks like your Toggle is refreshed before the List (possibly a bug, fixed in SwiftUI 2.0).

You can extract your row to another view and check if the index still exists.

struct ContentView: View {
    @ObservedObject var vm: ViewModel = .init()

    var body: some View {
        List(vm.arr.indices, id: \.self) { index in
            ToggleView(vm: self.vm, index: index)
        }
    }
}

struct ToggleView: View {
    @ObservedObject var vm: ViewModel
    let index: Int
    
    @ViewBuilder
    var body: some View {
        if index < vm.arr.count {
            Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
        }
    }
}

This way the ToggleView will be refreshed after the List.

If you do the same but inside the ContentView it will still crash:

ContentView {
    ...
    @ViewBuilder
    func toggleView(forIndex index: Int) -> some View {
        if index < vm.arr.count {
            Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
        }
    }
}

Tags:

Swift

Swiftui