I am trying to use a ForEach
loop to dynamically add textfields inside a form section with a button.
Form {
ForEach(0..<numberOfItems, id: \.self) { _ in
TextField("", text: $listItemEntry)
}
Button("Add Item") {
numberOfItems = numberOfItems 1
}
}
How would I replace the $listItemEntry
which is currently bound to a state variable so that I can individually type in each text field. As a bonus, what would be the best way to save these entries to CoreData, as I don't really know where to start.
CodePudding user response:
The issue you are having boils down to trying to figure this out from demonstration code. You should really avoid using indices in Lists
if at all possible, though there are some workarounds. In this case, the easiest thing to do is to create a struct
conforming to Identifiable
as your data model, instead of using a plain string. This greatly simplifies your code:
struct TextFieldDynamicAdd: View {
// I made this a @State var, but it could just as easily be a view model
@State var listItemEntries: [ListItemEntry] = []
var body: some View {
Form {
// Since you need to use a binding inside the ForEach, you need to use $listItemEntries
// and $listItemEntry in your declaration. Since ListItemEntry is Identifiable, no need for
// a id: \.self.
ForEach($listItemEntries) { $listItemEntry in
TextField("", text: $listItemEntry.text)
}
Button("Add Item") {
listItemEntries.append(ListItemEntry(text: "Item \(listItemEntries.count 1)"))
}
}
}
}
struct ListItemEntry: Identifiable {
let id = UUID()
var text: String
}
While it is a bit outside of the scope of the question, the ForEach
for a Core Data entity would be defined as:
ForEach(listItemEntries) { listItemEntry in
ASSUMING the attribute was non-optional or you created a wrapper variable that always returned a non-optional. Core Data entities conform to Identifiable
and ObservableObject
so you drop the $
in the ForEach
. This is true using any ObservableObject
in a ForEach
.
You should avoid using id: \.self
in ForEach
like the plague. Because these are not Identifiable, the OS can get confused when doing moves, deletes or if you have two otherwise identical values and crash.
CodePudding user response:
The approach you can follow is to store the values in a view model, which has to be a class of type ObservableObject
. You can use a dictionary to always find the values based on their index.
To individually have a @State
variable for each text field, just use a separated view, with a dedicated variable. This subview will read the same view model, therefore also the same dictionary, and will contain the function to store the value.
Iterate over the keys of the dictionary in the ForEach
of your main view, and call the subview that contains the text field.
Here is the code, for you to improve based on your program:
// Your view model
class ViewModel: ObservableObject {
// Variable of dictionary type
@Published var list = [Int: String]()
}
// Dedicated view for each text field
struct Field: View {
// Read the view model, to store the value of the text field
@EnvironmentObject var viewModel: ViewModel
// Index: where in the dictionary the value will be stored
let index: Int
// Dedicated state var for each field
@State private var field = ""
var body: some View {
VStack {
TextField("Enter text:", text: $field)
Button("Confirm", action: store)
}
}
// Store the value in the dictionary
private func store() {
viewModel.list[index] = field
}
}
struct Example: View {
// Create the view model instance
@StateObject var viewModel = ViewModel()
var body: some View {
Form {
// Read the keys of the dictionary
ForEach(viewModel.list.keys.sorted(), id: \.self) { index in
VStack {
Text(viewModel.list[index] ?? "")
.foregroundColor(.green)
Field(index: index)
// Pass the view model instance to the Field
.environmentObject(viewModel)
}
}
Button("Add one item") {
// Add the next key with an empty string
let currentCount = viewModel.list.count
viewModel.list[currentCount 1] = ""
}
}
}
}