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.

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)
}
}
class DynamicFormViewModel: ObservableObject {
@Published var persons: [SectionType]
...
init(persons: [SectionType]) {
self.persons = persons
}
...
}
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
- Maek Moeykens, SwiftUI Views Mastery
Big Mountain Studio book
- SwiftUI / Binding
Apple Official Documentation