Home > OS >  Dynamically add textfields with a for each loop and a button in swiftUI
Dynamically add textfields with a for each loop and a button in swiftUI

Time:05-07

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] = ""
            }
        }
    }
}
  • Related