A SwiftUI implementation of React Hooks Form.
Performant, flexible and extensible forms with easy-to-use validation.
SwiftUI Hooks Form is a Swift implementation of React Hook Form
This library continues working from SwiftUI Hooks. Thank ra1028 for developing the library.
| Minimum Version | |
|---|---|
| Swift | 5.7 |
| Xcode | 14.0 |
| iOS | 13.0 |
| macOS | 10.15 |
| tvOS | 13.0 |
The module name of the package is FormHook. Choose one of the instructions below to install and add the following import statement to your source code.
import FormHookFrom Xcode menu: File > Swift Packages > Add Package Dependency
https://github.com/dungntm58/swiftui-hooks-form In your Package.swift file, first add the following to the package dependencies:
.package(url: "https://github.com/dungntm58/swiftui-hooks-form"),And then, include "Hooks" as a dependency for your target:
.target(name: "<target>", dependencies: [ .product(name: "FormHook", package: "swiftui-hooks-form"), ]),👇 Click to open the description.
useForm
func useForm<FieldName>( mode: Mode = .onSubmit, reValidateMode: ReValidateMode = .onChange, resolver: Resolver<FieldName>? = nil, context: Any? = nil, shouldUnregister: Bool = true, criteriaMode: CriteriaMode = .all, delayErrorInNanoseconds: UInt64 = 0 ) -> FormControl<FieldName> where FieldName: HashableuseForm is a custom hook for managing forms with ease. It returns a FormControl instance.
useController
func useController<FieldName, Value>( name: FieldName, defaultValue: Value, rules: any Validator<Value>, shouldUnregister: Bool = false ) -> ControllerRenderOption<FieldName, Value> where FieldName: HashableThis custom hook powers Controller. Additionally, it shares the same props and methods as Controller. It's useful for creating reusable Controlled input.
useController must be called in a Context scope.
enum FieldName: Hashable { case username case password } @ViewBuilder var hookBody: some View { let form: FormControl<FieldName> = useForm() Context.Provider(value: form) { let (field, fieldState, formState) = useController(name: FieldName.username, defaultValue: "") TextField("Username", text: field.value) } } // this code achieves the same @ViewBuilder var body: some View { ContextualForm(...) { form in let (field, fieldState, formState) = useController(name: FieldName.username, defaultValue: "") TextField("Username", text: field.value) } }👇 Click to open the description.
ContextualForm
struct ContextualForm<Content, FieldName>: View where Content: View, FieldName: Hashable { init(mode: Mode = .onSubmit, reValidateMode: ReValidateMode = .onChange, resolver: Resolver<FieldName>? = nil, context: Any? = nil, shouldUnregister: Bool = true, shouldFocusError: Bool = true, delayErrorInNanoseconds: UInt64 = 0, @_implicitSelfCapture onFocusField: @escaping (FieldName) -> Void, @ViewBuilder content: @escaping (FormControl<FieldName>) -> Content ) @available(macOS 12.0, iOS 15.0, tvOS 15.0, *) init(mode: Mode = .onSubmit, reValidateMode: ReValidateMode = .onChange, resolver: Resolver<FieldName>? = nil, context: Any? = nil, shouldUnregister: Bool = true, shouldFocusError: Bool = true, delayErrorInNanoseconds: UInt64 = 0, focusedFieldBinder: FocusState<FieldName?>.Binding, @ViewBuilder content: @escaping (FormControl<FieldName>) -> Content ) It wraps a call of useForm inside the hookBody and passes the FormControl value to a Context.Provider<Form>
It is identical to
let form: FormControl<FieldName> = useForm(...) Context.Provider(value: form) { ... }Controller
import SwiftUI struct Controller<Content, FieldName, Value>: View where Content: View, FieldName: Hashable { init( name: FieldName, defaultValue: Value, rules: any Validator<Value> = NoopValidator(), @ViewBuilder render: @escaping (ControllerRenderOption<FieldName, Value>) -> Content ) }struct FieldOption<FieldName, Value> { let name: FieldName let value: Binding<Value> }typealias ControllerRenderOption<FieldName, Value> = (field: FieldOption<FieldName, Value>, fieldState: FieldState, formState: FormState<FieldName>) where FieldName: HashableIt wraps a call of useController inside the hookBody. Like useController, you guarantee Controller must be used in a Context scope.
import SwiftUI import FormHook enum FieldName: Hashable { case email case password } struct LoginForm: View { var body: some View { ContextualForm { form in VStack(spacing: 16) { Controller( name: FieldName.email, defaultValue: "", rules: CompositeValidator( validators: [ RequiredValidator(), EmailValidator() ] ) ) { (field, fieldState, formState) in VStack(alignment: .leading) { TextField("Email", text: field.value) .textFieldStyle(RoundedBorderTextFieldStyle()) if let error = fieldState.error?.first { Text(error) .foregroundColor(.red) .font(.caption) } } } Controller( name: FieldName.password, defaultValue: "", rules: CompositeValidator( validators: [ RequiredValidator(), MinLengthValidator(length: 8) ] ) ) { (field, fieldState, formState) in VStack(alignment: .leading) { SecureField("Password", text: field.value) .textFieldStyle(RoundedBorderTextFieldStyle()) if let error = fieldState.error?.first { Text(error) .foregroundColor(.red) .font(.caption) } } } Button("Login") { Task { try await form.handleSubmit { values, errors in print("Login successful:", values) } } } .disabled(!formState.isValid) } .padding() } } }import SwiftUI import FormHook struct RegistrationForm: View { @FocusState private var focusedField: FieldName? var body: some View { ContextualForm( focusedFieldBinder: $focusedField ) { form in VStack(spacing: 20) { // Form fields here... Button("Register") { Task { do { try await form.handleSubmit( onValid: { values, _ in await registerUser(values) }, onInvalid: { _, errors in print("Validation errors:", errors) } ) } catch { print("Registration failed:", error) } } } .disabled(formState.isSubmitting) } } } private func registerUser(_ values: FormValue<FieldName>) async { // Registration logic } }- Validation: Use async validators for network-dependent validation
- Field Registration: Prefer
useControllerover direct field registration for better performance - Focus Management: Utilize the built-in focus management for better UX
- Error Handling: Implement proper error boundaries for production apps
- Module Rename: The module is now called
FormHookinstead ofHooks - File Structure: Internal files have been reorganized for better maintainability
- Type Safety: Improved type safety with better generic constraints
-
Update your import statements:
// Before import Hooks // After import FormHook
-
API References remain the same - no changes needed to your form implementations
-
If you were importing internal types, they may have moved:
Types.swift→FormTypes.swift- Form-related types are now in dedicated files
- Enhanced Type Safety: Better compile-time type checking
- Improved Validation: Consolidated validation patterns for better performance
- Better Error Messages: More descriptive error messages and debugging info
- Import Errors: Make sure you're importing
FormHooknotHooks - Field Focus: Use
FocusStatebinding for iOS 15+ focus management - Validation Performance: Consider using
delayErrorInNanosecondsfor expensive validations
- Check the API Reference
- Look at Example implementations
- File issues on GitHub