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