Skip to content

Commit cce2103

Browse files
committed
Refactor and update docs
1 parent 0c47e61 commit cce2103

File tree

10 files changed

+938
-709
lines changed

10 files changed

+938
-709
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ DerivedData/
88
.swiftpm/config/registries.json
99
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
1010
.netrc
11+
.claude

README.md

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ And then, include "Hooks" as a dependency for your target:
7373

7474
### Documentation
7575

76-
- [API Reference](https://dungntm58.github.io/swiftui-hooks-form/documentation/hooks)
77-
- [Example apps](Examples)
76+
- [API Reference](https://dungntm58.github.io/swiftui-hooks-form/documentation/formhook)
77+
- [Example apps](Example)
78+
- [Migration Guide](#migration-guide)
7879

7980
---
8081

@@ -229,6 +230,185 @@ It wraps a call of `useController` inside the `hookBody`. Like `useController`,
229230

230231
---
231232

233+
## Examples
234+
235+
### Basic Form with Validation
236+
237+
```swift
238+
import SwiftUI
239+
import FormHook
240+
241+
enum FieldName: Hashable {
242+
case email
243+
case password
244+
}
245+
246+
struct LoginForm: View {
247+
var body: some View {
248+
ContextualForm { form in
249+
VStack(spacing: 16) {
250+
Controller(
251+
name: FieldName.email,
252+
defaultValue: "",
253+
rules: CompositeValidator(
254+
validators: [
255+
RequiredValidator(),
256+
EmailValidator()
257+
]
258+
)
259+
) { (field, fieldState, formState) in
260+
VStack(alignment: .leading) {
261+
TextField("Email", text: field.value)
262+
.textFieldStyle(RoundedBorderTextFieldStyle())
263+
264+
if let error = fieldState.error?.first {
265+
Text(error)
266+
.foregroundColor(.red)
267+
.font(.caption)
268+
}
269+
}
270+
}
271+
272+
Controller(
273+
name: FieldName.password,
274+
defaultValue: "",
275+
rules: CompositeValidator(
276+
validators: [
277+
RequiredValidator(),
278+
MinLengthValidator(length: 8)
279+
]
280+
)
281+
) { (field, fieldState, formState) in
282+
VStack(alignment: .leading) {
283+
SecureField("Password", text: field.value)
284+
.textFieldStyle(RoundedBorderTextFieldStyle())
285+
286+
if let error = fieldState.error?.first {
287+
Text(error)
288+
.foregroundColor(.red)
289+
.font(.caption)
290+
}
291+
}
292+
}
293+
294+
Button("Login") {
295+
Task {
296+
try await form.handleSubmit { values, errors in
297+
print("Login successful:", values)
298+
}
299+
}
300+
}
301+
.disabled(!formState.isValid)
302+
}
303+
.padding()
304+
}
305+
}
306+
}
307+
```
308+
309+
### Advanced Form with Custom Validation
310+
311+
```swift
312+
import SwiftUI
313+
import FormHook
314+
315+
struct RegistrationForm: View {
316+
@FocusState private var focusedField: FieldName?
317+
318+
var body: some View {
319+
ContextualForm(
320+
focusedFieldBinder: $focusedField
321+
) { form in
322+
VStack(spacing: 20) {
323+
// Form fields here...
324+
325+
Button("Register") {
326+
Task {
327+
do {
328+
try await form.handleSubmit(
329+
onValid: { values, _ in
330+
await registerUser(values)
331+
},
332+
onInvalid: { _, errors in
333+
print("Validation errors:", errors)
334+
}
335+
)
336+
} catch {
337+
print("Registration failed:", error)
338+
}
339+
}
340+
}
341+
.disabled(formState.isSubmitting)
342+
}
343+
}
344+
}
345+
346+
private func registerUser(_ values: FormValue<FieldName>) async {
347+
// Registration logic
348+
}
349+
}
350+
```
351+
352+
---
353+
354+
## Performance Guidelines
355+
356+
- **Validation**: Use async validators for network-dependent validation
357+
- **Field Registration**: Prefer `useController` over direct field registration for better performance
358+
- **Focus Management**: Utilize the built-in focus management for better UX
359+
- **Error Handling**: Implement proper error boundaries for production apps
360+
361+
---
362+
363+
## Migration Guide
364+
365+
### From Previous Versions
366+
367+
#### Breaking Changes in v2.0
368+
369+
1. **Module Rename**: The module is now called `FormHook` instead of `Hooks`
370+
2. **File Structure**: Internal files have been reorganized for better maintainability
371+
3. **Type Safety**: Improved type safety with better generic constraints
372+
373+
#### Migration Steps
374+
375+
1. Update your import statements:
376+
```swift
377+
// Before
378+
import Hooks
379+
380+
// After
381+
import FormHook
382+
```
383+
384+
2. API References remain the same - no changes needed to your form implementations
385+
386+
3. If you were importing internal types, they may have moved:
387+
- `Types.swift` `FormTypes.swift`
388+
- Form-related types are now in dedicated files
389+
390+
#### New Features
391+
392+
- **Enhanced Type Safety**: Better compile-time type checking
393+
- **Improved Validation**: Consolidated validation patterns for better performance
394+
- **Better Error Messages**: More descriptive error messages and debugging info
395+
396+
### Troubleshooting
397+
398+
#### Common Issues
399+
400+
1. **Import Errors**: Make sure you're importing `FormHook` not `Hooks`
401+
2. **Field Focus**: Use `FocusState` binding for iOS 15+ focus management
402+
3. **Validation Performance**: Consider using `delayErrorInNanoseconds` for expensive validations
403+
404+
#### Getting Help
405+
406+
- Check the [API Reference](https://dungntm58.github.io/swiftui-hooks-form/documentation/formhook)
407+
- Look at [Example implementations](Example)
408+
- File issues on [GitHub](https://github.com/dungntm58/swiftui-hooks-form/issues)
409+
410+
---
411+
232412
## Acknowledgements
233413

234414
- [React Hooks](https://reactjs.org/docs/hooks-intro.html)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//
2+
// ContextualForm.swift
3+
// swiftui-hooks-form
4+
//
5+
// Created by Robert on 06/11/2022.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import Hooks
11+
12+
/// A convenient view that wraps a call of `useForm`.
13+
public struct ContextualForm<Content, FieldName>: View where Content: View, FieldName: Hashable {
14+
let formOptions: FormOption<FieldName>
15+
let contentBuilder: (FormControl<FieldName>) -> Content
16+
17+
/// Initialize a `ContextualForm`
18+
/// - Parameters:
19+
/// - mode: The mode in which the form will be validated. Defaults to `.onSubmit`.
20+
/// - reValidateMode: The mode in which the form will be re-validated. Defaults to `.onChange`.
21+
/// - resolver: A resolver used to resolve validation rules for fields. Defaults to `nil`.
22+
/// - context: An optional context that can be used when resolving validation rules for fields. Defaults to `nil`.
23+
/// - shouldUnregister: A boolean value that indicates whether the form should unregister its fields when it is deallocated. Defaults to `true`.
24+
/// - shouldFocusError: A boolean value that indicates whether the form should focus on an error field when it is invalidated. Defaults to `true`.
25+
/// - delayErrorInNanoseconds: The amount of time (in nanoseconds) that the form will wait before focusing on an error field when it is invalidated. Defaults to 0 nanoseconds (no delay).
26+
/// - onFocusField: An action performed when a field is focused on by the user or programmatically by the form.
27+
/// - contentBuilder: A closure used for building content for the contextual form view, using a FormControl<FieldName> instance as a parameter.
28+
public init(
29+
mode: Mode = .onSubmit,
30+
reValidateMode: ReValidateMode = .onChange,
31+
resolver: Resolver<FieldName>? = nil,
32+
context: Any? = nil,
33+
shouldUnregister: Bool = true,
34+
shouldFocusError: Bool = true,
35+
delayErrorInNanoseconds: UInt64 = 0,
36+
@_implicitSelfCapture onFocusField: @escaping (FieldName) -> Void,
37+
@ViewBuilder content: @escaping (FormControl<FieldName>) -> Content
38+
) {
39+
self.formOptions = .init(
40+
mode: mode,
41+
reValidateMode: reValidateMode,
42+
resolver: resolver,
43+
context: context,
44+
shouldUnregister: shouldUnregister,
45+
shouldFocusError: shouldFocusError,
46+
delayErrorInNanoseconds: delayErrorInNanoseconds,
47+
onFocusField: onFocusField
48+
)
49+
self.contentBuilder = content
50+
}
51+
52+
/// Initialize a `ContextualForm`
53+
/// - Parameters:
54+
/// - mode: The mode in which the form will be validated. Defaults to `.onSubmit`.
55+
/// - reValidateMode: The mode in which the form will be re-validated. Defaults to `.onChange`.
56+
/// - resolver: A resolver used to resolve validation rules for fields. Defaults to `nil`.
57+
/// - context: An optional context that can be used when resolving validation rules for fields. Defaults to `nil`.
58+
/// - shouldUnregister: A boolean value that indicates whether the form should unregister its fields when it is deallocated. Defaults to `true`.
59+
/// - shouldFocusError: A boolean value that indicates whether the form should focus on an error field when it is invalidated. Defaults to `true`.
60+
/// - delayErrorInNanoseconds: The amount of time (in nanoseconds) that the form will wait before focusing on an error field when it is invalidated. Defaults to 0 nanoseconds (no delay).
61+
/// - focusedFieldBinder: A binding used to bind a FocusState<FieldName?> instance, which holds information about which field is currently focused on by the user or programmatically by the form.
62+
/// - contentBuilder: A closure used for building content for the contextual form view, using a FormControl<FieldName> instance as a parameter.
63+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
64+
public init(
65+
mode: Mode = .onSubmit,
66+
reValidateMode: ReValidateMode = .onChange,
67+
resolver: Resolver<FieldName>? = nil,
68+
context: Any? = nil,
69+
shouldUnregister: Bool = true,
70+
shouldFocusError: Bool = true,
71+
delayErrorInNanoseconds: UInt64 = 0,
72+
focusedFieldBinder: FocusState<FieldName?>.Binding,
73+
@ViewBuilder content: @escaping (FormControl<FieldName>) -> Content
74+
) {
75+
self.formOptions = .init(
76+
mode: mode,
77+
reValidateMode: reValidateMode,
78+
resolver: resolver,
79+
context: context,
80+
shouldUnregister: shouldUnregister,
81+
shouldFocusError: shouldFocusError,
82+
delayErrorInNanoseconds: delayErrorInNanoseconds,
83+
focusedStateBinder: focusedFieldBinder
84+
)
85+
self.contentBuilder = content
86+
}
87+
88+
public var body: some View {
89+
HookScope {
90+
let form = useForm(formOptions)
91+
Context.Provider(value: form) {
92+
contentBuilder(form)
93+
}
94+
}
95+
}
96+
}

Sources/FormHook/Controller.swift

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,6 @@ import Foundation
99
import SwiftUI
1010
import Hooks
1111

12-
/// A type that represents a field option.
13-
///
14-
/// The `FieldOption` type is used to represent a field option. It consists of a `name` and a `value` of type `Binding<Value>`. The `Binding` type is used to create two-way bindings between a view and its underlying model.
15-
public struct FieldOption<FieldName, Value> {
16-
/// The name of the field option.
17-
public let name: FieldName
18-
/// A binding of type `Value`.
19-
public let value: Binding<Value>
20-
21-
init(name: FieldName, value: Binding<Value>) {
22-
self.name = name
23-
self.value = value
24-
}
25-
}
26-
27-
public typealias ControllerRenderOption<FieldName, Value> = (field: FieldOption<FieldName, Value>, fieldState: FieldState, formState: FormState<FieldName>) where FieldName: Hashable
28-
2912
/// A convenient view that wraps a call of `useController`
3013
public struct Controller<Content, FieldName, Value>: View where Content: View, FieldName: Hashable {
3114
let form: FormControl<FieldName>?
@@ -62,7 +45,7 @@ public struct Controller<Content, FieldName, Value>: View where Content: View, F
6245
self.defaultValue = defaultValue
6346
self.rules = rules
6447
self.render = render
65-
self.shouldUnregister = true
48+
self.shouldUnregister = shouldUnregister
6649
self.unregisterOption = unregisterOption
6750
self.fieldOrdinal = fieldOrdinal
6851
}

Sources/FormHook/FieldTypes.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// FieldTypes.swift
3+
// swiftui-hooks-form
4+
//
5+
// Created by Robert on 06/11/2022.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import Hooks
11+
12+
/// A type that represents a field option.
13+
///
14+
/// The `FieldOption` type is used to represent a field option. It consists of a `name` and a `value` of type `Binding<Value>`. The `Binding` type is used to create two-way bindings between a view and its underlying model.
15+
public struct FieldOption<FieldName, Value> {
16+
/// The name of the field option.
17+
public let name: FieldName
18+
/// A binding of type `Value`.
19+
public let value: Binding<Value>
20+
21+
init(name: FieldName, value: Binding<Value>) {
22+
self.name = name
23+
self.value = value
24+
}
25+
}
26+
27+
/// A tuple representing the render options for a controller.
28+
public typealias ControllerRenderOption<FieldName, Value> = (field: FieldOption<FieldName, Value>, fieldState: FieldState, formState: FormState<FieldName>) where FieldName: Hashable

0 commit comments

Comments
 (0)