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 FormHook
From 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: Hashable
useForm
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: Hashable
This 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: Hashable
It 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
useController
over 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
FormHook
instead 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
FormHook
notHooks
- Field Focus: Use
FocusState
binding for iOS 15+ focus management - Validation Performance: Consider using
delayErrorInNanoseconds
for expensive validations
- Check the API Reference
- Look at Example implementations
- File issues on GitHub