3

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

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

I am relieved to learn that it is a bug. Hope next version will fix it! It already works when using List and onDelete.
1

I can not perfectly explain what is causing that crash, but I've been able to reproduce the error and it looks like after deleting a field,SwiftUI is still looking for all indices and when it is trying to access the element at a deleted index, it's unable to find it which causes the index out of bounds error.

To fix that, we can write a conditional statement to make sure an element is searched only if its index is included in the collection of indices.

FieldView(value: Binding<String>(get: {
    if viewModel.items.indices.contains(index) {
        return viewModel.items[index]
    } else {
        return ""
    }
}, set: { newValue in
    viewModel.items[index] = newValue
})) {
    viewModel.items.remove(at: index)
}

The above solution solves the problem since it makes sure that the element will not be searched when the number of elements (items.count) is not greater than the index.

This is just what I've been able to understand, but something else might be happening under the hood.

1 Comment

This is also a valid answer! Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.