Swift – Using ForEach with a an array of Bindings (SwiftUI)

swiftswiftui

My objective is to dynamically generate a form from JSON. I've got everything put together except for generating the FormField views (TextField based) with bindings to a dynamically generated list of view models.

If I swap out the FormField views for just normal Text views it works fine (see screenshot):

ForEach(viewModel.viewModels) { vm in
    Text(vm.placeholder)
}

for

ForEach(viewModel.viewModels) { vm in
     FormField(viewModel: $vm)
}

I've tried to make the viewModels property of ConfigurableFormViewModel an @State var, but it loses its codability. JSON > Binding<[FormFieldViewModel] naturally doesn't really work.

Here's the gist of my code:

Screenshot of form from JSON but using <code>Text</code>

Best Answer

The first thing that you can try is this:

ForEach(0 ..< numberOfItems) { index in
   HStack {
     TextField("PlaceHolder", text: Binding(
       get: { return items[index] },
       set: { (newValue) in return self.items[index] = newValue}
     ))
   }
}

The problem with the previous approach is that if numberOfItems is some how dynamic and could change because of an action of a Button for example, it is not going to work and it is going to throw the following error: ForEach<Range<Int>, Int, HStack<TextField<Text>>> count (3) != its initial count (0). 'ForEach(_:content:)' should only be used for *constant* data. Instead conform data to 'Identifiable' or use 'ForEach(_:id:content:)' and provide an explicit 'id'!

If you have that use case, you can do something like this, it will work even if the items are increasing or decreasing during the lifecycle of the SwiftView:

ForEach(items.indices, id:\.self ){ index in
   HStack {
     TextField("PlaceHolder", text: Binding(
       get: { return items[index] },
       set: { (newValue) in return self.items[index] = newValue}
     ))
   }
}