When programming a form in SwiftUI, the typical case involves forms with a fixed number of fields. These are forms like the ones you use when registering on a website. However, this is not the only type of form you might encounter. Sometimes, you may need to create forms that collect data for multiple entities, and these entities might not always be of the same type. For example, consider forms for booking a train or flight ticket, where different sections might be required for passengers, payment, and additional services.

The approach to implementing dynamic, variable-section forms is quite different, as it involves working with Dynamic Bindings. In this post, you'll learn how to handle this complexity effectively. By the end of the post, you'll find a link to a GitHub repository containing the base code for this project.

Dynamic sample SwiftUI app

The sample app follows the MVVM architecture and implements a form for managing multiple persons. Each person is represented as a separate section in the form, and they can either be an Adult or a Child. Adults have fields for name, surname, and email, while Children have fields for name, surname, and birthdate. Validation rules are implemented, such as ensuring that a child’s age is under 18 years and that email addresses follow the correct syntax.

We are going to create a person form for 2 adults and 1 child:
struct ContentView: View {
    @StateObject private var viewModel = DynamicFormViewModel(persons: [
        .adult(Adult(name: "Juan", surename: "Pérez", email: "juan.perez@example.com")),
        .child(Child(name: "Carlos", surename: "Gomez", birthdate: Date(timeIntervalSince1970: 1452596356))),
        .adult(Adult(name: "Ana", surename: "Lopez", email: "ana.lopez@example.com"))
    ])
    
    var body: some View {
        DynamicFormView(viewModel: viewModel)
    }
}
At this point in view model we start to see different things
class DynamicFormViewModel: ObservableObject {
    @Published var persons: [SectionType]
...
    init(persons: [SectionType]) {
        self.persons = persons
    }
...
}
Instead of having one @published attribute per field we have have an array of SectionType. 
struct Adult: Identifiable {
    var id = UUID()
    var name: String
    var surename: String
    var email: String
}

struct Child: Identifiable {
    var id = UUID()
    var name: String
    var surename: String
    var birthdate: Date
}

enum SectionType {
    case adult(Adult)
    case child(Child)
}

SectionType is an enum (struct)  that could be Adult or a Child. Our job in the View now will be to create a new binding to attach to the current form field that is being rendered:

struct DynamicFormView: View {
    @StateObject var viewModel: DynamicFormViewModel

    var body: some View {
        Form {
            ForEach(Array(viewModel.persons.enumerated()), id: \.offset) { index, persona in
                Section {
                    if let adultoBinding = adultBinding(for: index) {
                        AdultForm(adulto: adultoBinding)
                            .environmentObject(viewModel)
                    }
                    if let niñoBinding = childBinding(for: index) {
                        ChildForm(niño: niñoBinding)
                            .environmentObject(viewModel)
                    }
                }
            }
        }
    }

    private func adultBinding(for index: Int) -> Binding<Adult>? {
        guard case .adult(let adult) = viewModel.persons[index] else { return nil }
        return Binding<Adult>(
            get: { adult },
            set: { newAdult in viewModel.persons[index] = .adult(newAdult) }
        )
    }

    private func childBinding(for index: Int) -> Binding<Child>? {
        guard case .child(let child) = viewModel.persons[index] else { return nil }
        return Binding<Child>(
            get: { child },
            set: { newChild in viewModel.persons[index] = .child(newChild) }
        )
    }
}

The DynamicFormView dynamically renders a SwiftUI form where each section corresponds to a person from a DynamicFormViewModel‘s persons array, which contains enums distinguishing adults and children. Using helper methods, it creates Binding objects to provide two-way bindings for either an AdultForm or ChildForm based on the person’s type. These forms allow editing of the Adult or Child data directly in the view model. By leveraging SwiftUI’s ForEach, conditional views, and @EnvironmentObject, the view efficiently handles heterogeneous collections and updates the UI in response to changes.

struct DynamicFormView: View {
    @StateObject var viewModel: DynamicFormViewModel

    var body: some View {
        Form {
            ForEach(Array(viewModel.persons.enumerated()), id: \.offset) { index, persona in
                Section {
                    if let adultoBinding = adultBinding(for: index) {
                        AdultForm(adulto: adultoBinding)
                            .environmentObject(viewModel)
                    }
                    if let niñoBinding = childBinding(for: index) {
                        ChildForm(niño: niñoBinding)
                            .environmentObject(viewModel)
                    }
                }
            }
        }
    }

    private func adultBinding(for index: Int) -> Binding<Adult>? {
        guard case .adult(let adult) = viewModel.persons[index] else { return nil }
        return Binding<Adult>(
            get: { adult },
            set: { newAdult in viewModel.persons[index] = .adult(newAdult) }
        )
    }

    private func childBinding(for index: Int) -> Binding<Child>? {
        guard case .child(let child) = viewModel.persons[index] else { return nil }
        return Binding<Child>(
            get: { child },
            set: { newChild in viewModel.persons[index] = .child(newChild) }
        )
    }
}

Finally, the implementation of AdultSectionForm (and ChildSectionForm) is nothing special and not commonly encountered in standard SwiftUI form development.

struct AdultSectionForm: View {
    @Binding var adulto: Adult
    @EnvironmentObject var viewModel: DynamicFormViewModel
    
    var body: some View {
        VStack(alignment: .leading) {
            TextField("Name", text: $adulto.name)
                .onChange(of: adulto.name) { newValue, _ in
                    viewModel.validateName(adultoId: adulto.id, nombre: newValue)
                }
            if let isValid = viewModel.validName[adulto.id], !isValid {
                Text("Name cannot be empty.")
                    .foregroundColor(.red)
            }
            
            TextField("Surename", text: $adulto.surename)
            
            TextField("Email", text: $adulto.email)
                .onChange(of: adulto.email) { newValue, _ in
                    viewModel.validateEmail(adultoId: adulto.id, email: newValue)
                }
            if let isValido = viewModel.validEmail[adulto.id], !isValido {
                Text("Not valid email")
                    .foregroundColor(.red)
            }
        }
    }
}

Conclusions

Handling dynamic forms in SwiftUI is slightly different from what is typically explained in books or basic tutorials. While it isn’t overly complicated, it does require a clear understanding, especially when implementing a form with such characteristics.

In this post, I have demonstrated a possible approach to implementing dynamic forms. You can find the source code used for this post in the repository linked below.

References

Copyright © 2024-2025 JaviOS. All rights reserved