Search code examples
swiftuiswiftui-foreach

Using TextField with ForEach in SwiftUI


I am trying to display a dynamic list of text fields using a ForEach. The following code is working as expected: I can add/remove text fields, and the binding is correct. However, when I move the items in a ObservableObject view model, it does not work anymore and it crashes with an index out of bounds error. Why is that? How can I make it work?

struct ContentView: View {
    @State var items = ["A", "B", "C"]
    
    var body: some View {
        VStack {
            ForEach(items.indices, id: \.self) { index in
                FieldView(value: Binding<String>(get: {
                    items[index]
                }, set: { newValue in
                    items[index] = newValue
                })) {
                    items.remove(at: index)
                }
            }
            Button("Add") {
                items.append("")
            }
        }
    }
}

struct FieldView: View {
    @Binding var value: String
    let onDelete: () -> Void
    
    var body: some View {
        HStack {
            TextField("item", text: $value)
            Button(action: {
                onDelete()
            }, label: {
                Image(systemName: "multiply")
            })
        }
    }
}

The view model I am trying to use:

class ViewModel: Observable {
    @Published var items: [String]
}
@ObservedObject var viewModel: ViewModel

I found many questions dealing with the same problem but I could not make one work with my case. Some of them do not mention the TextField, some other are not working (anymore?).

Thanks a lot


Solution

  • By checking the bounds inside the Binding, you can solve the issue:

    struct ContentView: View {
        @ObservedObject var viewModel: ViewModel = ViewModel(items: ["A", "B", "C"])
        
        var body: some View {
            VStack {
                ForEach(viewModel.items.indices, id: \.self) { index in
                    FieldView(value: Binding<String>(get: {
                        guard index < viewModel.items.count else { return "" } // <- HERE
                        return viewModel.items[index]
                    }, set: { newValue in
                        viewModel.items[index] = newValue
                    })) {
                        viewModel.items.remove(at: index)
                    }
                }
                Button("Add") {
                    viewModel.items.append("")
                }
            }
        }
    }
    

    It is a SwiftUI bug, similar question to this for example.