Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delete a Binding from a list in SwiftUI

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?

like image 828
FloWy Avatar asked Oct 27 '25 14:10

FloWy


1 Answers

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")
                })
            }
        }))
}
like image 158
Philip Dukhov Avatar answered Oct 30 '25 07:10

Philip Dukhov



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!