Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using ForEach with a an array of Bindings (SwiftUI)

Tags:

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>

like image 261
erdos Avatar asked Jun 25 '19 16:06

erdos


People also ask

How do I use binding in SwiftUI?

In SwiftUI, you can create bindings in 2 ways: With the @Binding property wrapper, which creates a binding, but doesn't store it. With other property wrappers, like @State, which creates a binding, and also stores its value.

How do you write ForEach in SwiftUI?

ForEach in SwiftUI is a view struct in its own right, which means you can return it directly from your view body if you want. You provide it an array of items, and you may also need to tell SwiftUI how it can identify each of your items uniquely so it knows how to update them when values change.

What is two way binding in SwiftUI?

Two Way Binding allows the data to be transferred to change the initial look of viewController. If we are interested in creating a button that changes color when the user presses it, we need to state keyword and Two-way binding.


1 Answers

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}
     ))
   }
}
like image 163
Nicolas Yuste Avatar answered Sep 28 '22 04:09

Nicolas Yuste