# Composable Forms Composable form state, validation, and SwiftUI bindings for ComposableArchitecture2 features. `ComposableForms` gives a form a small reducer that owns the current value, focused field, validation state, and submission status. It supports synchronous and asynchronous validation, scoped child fields, debounced validation after observed changes, and SwiftUI helpers for rendering fields from a `StoreOf>`. ```swift import ComposableArchitecture2 import ComposableForms struct ProfileDraft: Equatable { var name = "" var email = "" } enum ProfileField { static let name = AnyHashable(\ProfileDraft.name) static let email = AnyHashable(\ProfileDraft.email) } typealias ProfileForm = Form ``` ## Installation Add the package to your Swift package dependencies: ```swift .package(url: "https://tangled.org/woody.fm/swift-composable-forms", branch: "main") ``` Then add `ComposableForms` to the targets that need it: ```swift .target( name: "YourFeature", dependencies: [ "ComposableForms", ] ) ``` ## Defining Fields Define the fields a form should observe and validate with `Form.Field`. ```swift let form = ProfileForm { Form.Field(\.name) { Validation(error: "Name is required") { !$0.isEmpty } } Form.Field(\.email) { Validation(error: "Email is required") { !$0.isEmpty } Validation(error: "Email is invalid") { $0.contains("@") } } } ``` Each field is identified by its key path by default. Pass an explicit `id` when you want stable domain identifiers or need to coordinate with view code. ```swift Form.Field(id: ProfileField.name, value: \.name) { Validation(error: "Name is required") { !$0.isEmpty } } ``` ## Validation Validations return `ValidationResult.valid` or one or more `ErrorMessage` values. Boolean convenience initializers cover common required-field checks: ```swift Validation(error: "Username is required") { !$0.isEmpty } ``` Use an async validation when the field needs to ask another dependency: ```swift Validation(policy: .formSubmitted) { username async throws in try await usernameIsAvailable(username) ? .valid : .invalid("Username is unavailable") } ``` Validation policies decide which user actions run a validation: ```swift ValidationPolicy.fieldSubmitted ValidationPolicy.focusLoss ValidationPolicy.formSubmitted ValidationPolicy.observedChange ValidationPolicy.userActions ValidationPolicy.all ``` Observed value changes are debounced through the `formObservedChangeValidationDelay` dependency, which tests can override with a `TestClock`. ## State `Form.State` stores the draft value and derived form state: ```swift var state = ProfileForm.State(value: ProfileDraft()) state.value.name = "Blob" state.focusedField = ProfileField.email state.validation[ProfileField.name] = .valid ``` The state exposes submission and validation helpers that views can use directly: ```swift state.canSubmit state.hasErrors state.isSubmitting state.isSubmitted state.isValidating state.validationErrors ``` When `Value` is `Equatable`, `Form.State` is also `Equatable`. ## Submission Send `.submittedForm` to validate all fields and submit the current value when the form is valid: ```swift let feature = ProfileForm { Form.Field(\.name) { Validation(error: "Name is required") { !$0.isEmpty } } } .onSubmit { value in try save(value) } ``` If any submit-time async validation is still running, the form remains in the `.submitting` state until those validations settle. ## SwiftUI Render a form with `ComposableForm` and field bindings: ```swift ComposableForm(store) { FormField(\.name) { $name in TextField("Name", text: $name) .submitLabel(.next) } FormField(\.email) { $email in TextField("Email", text: $email) .submitLabel(.done) } } ``` `FormField` submits its field when the SwiftUI field submits, which lets the form validate the field and advance focus to the next field. Use `FormScope` for nested values. Scoped view field IDs match scoped reducer field IDs. ```swift FormScope(\.profile) { FormField(\.displayName) { $displayName in TextField("Display name", text: $displayName) } } ``` ## Field Styles Customize field rendering with `FormFieldStyle`: ```swift struct InlineValidationStyle: FormFieldStyle { func makeBody(configuration: FormFieldStyleConfiguration) -> some View { VStack(alignment: .leading, spacing: 4) { configuration.content FormFieldValidationStatus(validation: configuration.validation) } } } ComposableForm(store) { FormField(\.name) { $name in TextField("Name", text: $name) } } .formFieldStyle(InlineValidationStyle()) ``` The default style renders the field content followed by validating and error messages. ## Requirements This package currently targets Swift 6.0 or later, iOS 17 or later, and macOS 14 or later. The core form reducer is separate from the SwiftUI helpers; view APIs are compiled only when SwiftUI is available.