Add TextFields inside a Form in MacOS SwiftUI

I want to build a MacOS form that allows for the end user to add textfields in the same way one can add rows to an excel table. The reason I want to use textfields is because I can't seem to find an example of a Table that will allow me to add text. 

I managed to get a basic setup using 'ForEach' and a button to append, however, when running the app from Xcode I am getting an error: 

"the ID  occurs multiple times within the collection, this will give undefined results!"

I realise that setting up unique ID's for each field is necessary, but I don't know how to set this up where the number of textfields that the end user will add can vary per form, and they are based on the append action. I even considered using the new 'GridRow' but it is only available for MacOS13 and above, and until Apple roles out with Ventura in late October I cannot implement this in the app. (no I do not want to run Ventura Beta as the machine this app will run on will only update MacOS when it officially roles out)

Here is my code: 


import SwiftUI

struct ContentView: View {

    @State private var name: [String] = []
    @State private var address: [String] = []
    @State private var telephoneNumber: [String] = []

    var body: some View {

        HStack {
            VStack{
                ForEach($name, id: \.self) {$name in TextField("Name", text:$name)
                }
                Button(action: {
                    name.append("")
                }) {
                    Text("Add Name")
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(Color(.systemGreen))
                }
            }
            .padding()

            VStack {
                ForEach($address, id: \.self) {$address in TextField("Address", text:$address)
                }
                Button(action: {
                    address.append("")
                }) {
                    Text("Add Address")
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(Color(.systemGreen))
                }
            }
            .padding()

            VStack {
                ForEach($telephoneNumber, id: \.self) {$telephoneNumber in
                    TextField("Telephone Number", text: $telephoneNumber)
                }
                Button(action: {
                    telephoneNumber.append("")
                }) {
                    Text("Add Telephone Number")
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(Color(.systemGreen))
                }
            }
            .padding()
        }
        Spacer()
        .frame(width: 600, height: 600)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

What is more is that when I type in one of the textfields, take 'name' textfield as an example, the additional 'name' textfields below are populated with the same text, and the textfield loses its focus after one letter is typed. 

Please will someone give me some guidance on how to correct this such that all additional textfields created are given a unique ID and save to the Form.

Answered by SulaymanAmir in 726172022

I worked it out: was an issue with the button action. See full code below:

import SwiftUI

struct ContentView: View {

    @State private var persons: [Person] = []

    var body: some View {
            VStack{
                ForEach($persons) { $person in TextField("Name", text: $person.name) }

Button(action: {
                    persons.append(Person.init(name: ""))
                }) {
                    Text("Add Name")
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(Color(.systemGreen))
                }
            .padding()
        }
        Spacer()
            .frame(width: 600, height: 600)
    }
}

struct Person: Identifiable {
    var id = UUID()
    var name: String
}

Thanks again!

The basic problem is that you're using id: \.self to identify ForEach elements, but this references the value of each String in your arrays, and these values don't meet the requirements for an id: they need to be stable/unchanging and unique.

Instead, you should create custom values that properly conform to Identifiable. For example, for your names (some details omitted):

struct Person: Identifiable {
    var id = UUID()
    var name: String
}

Then (more details omitted):

    @State private var persons: [Person] = []
     …
        HStack {
            VStack{
                ForEach($persons) { $person in TextField("Name", text: $person.name) }
                …

Note that, because the Person is now Identifiable, you don't need to specify id: at all, in the ForEach.

When the button is clicked to add a name, a new Person value is added to the persons array, and it has a unique id because UUID() returns a unique value every time it's called. Your names now have a stable, unique identity, and your list should now behave sensibly. You can do something similar for addresses and telephone numbers.

Note: Code typed directly into the forum post, so there may be typos. :)

Hello Polyphonic,

Thank you for your response. I have followed your input and appreciate your guidance. I have adjusted my code to the following:

import SwiftUI

struct ContentView: View {

    @State private var persons: [Person] = []

    var body: some View {

            VStack{
                ForEach($persons) { $person in TextField("Name", text: $person.name) }

                Button(action: {
                    persons.append(contentsOf: persons)
                }) {
                    Text("Add Name")
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(Color(.systemGreen))
                }
            .padding()
        }
        Spacer()
            .frame(width: 600, height: 600)
    }
}

struct Person: Identifiable {
    var id = UUID()
    var name: String
}


Now I think I've done something wrong at the append action in the button, because when I run the app no textfields are created on the click of the button. I know it has something to do with the parenthesis (contentsOf: persons)...

What am I not understanding?

Accepted Answer

I worked it out: was an issue with the button action. See full code below:

import SwiftUI

struct ContentView: View {

    @State private var persons: [Person] = []

    var body: some View {
            VStack{
                ForEach($persons) { $person in TextField("Name", text: $person.name) }

Button(action: {
                    persons.append(Person.init(name: ""))
                }) {
                    Text("Add Name")
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(Color(.systemGreen))
                }
            .padding()
        }
        Spacer()
            .frame(width: 600, height: 600)
    }
}

struct Person: Identifiable {
    var id = UUID()
    var name: String
}

Thanks again!

Add TextFields inside a Form in MacOS SwiftUI
 
 
Q