I am trying to simply delete an element from a list in Swift and SwiftUI. Without binding something in the ForEach loop, it does get removed. However, with binding something it crashes with an error Index out of range. It seems like the ForEach loop is constant, not updating, and trying to render at the specific index.
Example view code:
@ObservedObject var todoViewModel: TodoViewModel
//...
ForEach(self.todoViewModel.todos.indices) { index in
TextField("Test", text: self.$todoViewModel.todos[index].title)
.contextMenu(ContextMenu(menuItems: {
VStack {
Button(action: {
self.todoViewModel.deleteAt(index)
}, label: {
Label("Delete", systemImage: "trash")
})
}
}))
}
Example view model code:
final class TodoViewModel: ObservableObject {
@Published var todos: [Todo] = []
func deleteAt(_ index: Int) -> Void {
self.todos.remove(at: index)
}
}
Example model code:
struct Todo: Identifiable {
var id: Int
var title: String = ""
}
Does anyone know how to properly delete an element from a list where it is bound in a ForEach loop?
This happens because you're enumerating by indices and referencing binding by index inside ForEach
I suggest you switching to ForEachIndexed: this wrapper will pass both index and a correct binding to your block:
struct ForEachIndexed<Data: MutableCollection&RandomAccessCollection, RowContent: View, ID: Hashable>: View, DynamicViewContent where Data.Index : Hashable
{
var data: [(Data.Index, Data.Element)] {
forEach.data
}
let forEach: ForEach<[(Data.Index, Data.Element)], ID, RowContent>
init(_ data: Binding<Data>,
@ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
) where Data.Element: Identifiable, Data.Element.ID == ID {
forEach = ForEach(
Array(zip(data.wrappedValue.indices, data.wrappedValue)),
id: \.1.id
) { i, _ in
rowContent(i, Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
}
}
init(_ data: Binding<Data>,
id: KeyPath<Data.Element, ID>,
@ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
) {
forEach = ForEach(
Array(zip(data.wrappedValue.indices, data.wrappedValue)),
id: (\.1 as KeyPath<(Data.Index, Data.Element), Data.Element>).appending(path: id)
) { i, _ in
rowContent(i, Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
}
}
var body: some View {
forEach
}
}
Usage:
ForEachIndexed($todoViewModel.todos) { index, todoBinding in
TextField("Test", text: todoBinding.title)
.contextMenu(ContextMenu(menuItems: {
VStack {
Button(action: {
self.todoViewModel.deleteAt(index)
}, label: {
Label("Delete", systemImage: "trash")
})
}
}))
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With