Skip to content

Commit 0fa35df

Browse files
authored
Merge pull request #8 from MobileUpLLC/task/UPUP-1031-automatic-validation-of-all-fields-for-all-conditions
Automatic validation of all fields for all conditions
2 parents 1256f82 + 21b0213 commit 0fa35df

5 files changed

Lines changed: 58 additions & 11 deletions

File tree

ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,42 @@ import FormView
1111
struct ContentView: View {
1212
@ObservedObject var viewModel: ContentViewModel
1313

14+
@State private var isAllFieldValid = false
15+
1416
var body: some View {
1517
FormView(
1618
validate: [.manual, .onFieldValueChanged, .onFieldFocus],
17-
hideError: .onValueChanged
19+
hideError: .onValueChanged,
20+
isAllFieldValid: $isAllFieldValid
1821
) { proxy in
1922
FormField(
2023
value: $viewModel.name,
21-
rules: viewModel.nameValidationRules
24+
rules: viewModel.nameValidationRules,
25+
isRequired: true
2226
) { failedRules in
2327
TextInputField(title: "Name", text: $viewModel.name, failedRules: failedRules)
2428
}
2529
.disabled(viewModel.isLoading)
2630
FormField(
2731
value: $viewModel.age,
28-
rules: viewModel.ageValidationRules
32+
rules: viewModel.ageValidationRules,
33+
isRequired: false
2934
) { failedRules in
3035
TextInputField(title: "Age", text: $viewModel.age, failedRules: failedRules)
3136
}
3237
.disabled(viewModel.isLoading)
3338
FormField(
3439
value: $viewModel.pass,
35-
rules: viewModel.passValidationRules
40+
rules: viewModel.passValidationRules,
41+
isRequired: true
3642
) { failedRules in
3743
SecureInputField(title: "Password", text: $viewModel.pass, failedRules: failedRules)
3844
}
3945
.disabled(viewModel.isLoading)
4046
FormField(
4147
value: $viewModel.confirmPass,
42-
rules: viewModel.confirmPassValidationRules
48+
rules: viewModel.confirmPassValidationRules,
49+
isRequired: true
4350
) { failedRules in
4451
SecureInputField(title: "Confirm Password", text: $viewModel.confirmPass, failedRules: failedRules)
4552
}
@@ -52,7 +59,7 @@ struct ContentView: View {
5259
print("Form is valid: \(await proxy.validate())")
5360
}
5461
}
55-
.disabled(viewModel.isLoading)
62+
.disabled(isAllFieldValid == false || viewModel.isLoading)
5663
}
5764
.padding(.horizontal, 16)
5865
.padding(.top, 40)

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,18 @@ struct MyField: View {
5757
struct ContentView: View {
5858
@State var name: String = ""
5959

60+
@State private var isAllFieldValid = false
61+
6062
var body: some View {
6163
FormView( First failed field
6264
validate: [.manual], // Form will be validated on user action.
63-
hideError: .onValueChanged // Error for field wil be hidden on field value change.
65+
hideError: .onValueChanged, // Error for field wil be hidden on field value change.
66+
isAllFieldValid: $isAllFieldValid // Property indicating the result of validation of all fields without focus
6467
) { proxy in
6568
FormField(
6669
value: $name,
67-
rules: [ValidationRule.notEmpty(conditions: [.manual], message: "Name field should no be empty")]
70+
rules: [TextValidationRule.notEmpty(message: "Name field should no be empty")],
71+
isRequired: true, // field parameter, necessary for correct determination of validity of all fields
6872
) { failedRules in
6973
MyField(title: "Name", text: $name, failedRules: failedRules)
7074
}
@@ -73,6 +77,7 @@ struct ContentView: View {
7377
// Validate form on user action.
7478
print("Form is valid: \(proxy.validate())")
7579
}
80+
.disabled(isAllFieldValid == false) // Use isAllFieldValid to automatically disable the action button
7681
}
7782
}
7883
}
@@ -92,6 +97,9 @@ Error for each field gets hidden at one of three specific times:
9297
* `onFocus` - field with error is focused..
9398
* `onFucusLost` - field with error lost focus.
9499

100+
### Is All Field Valid
101+
Property indicating the result of validation of all fields without focus. Using this property you can additionally build ui update logic, for example block the next button.
102+
95103
### Custom Validation Rules
96104

97105
Extend `ValidationRule`:
@@ -146,7 +154,7 @@ FormView doesn't use any external dependencies.
146154
dependencies: [
147155
.package(
148156
url: "https://github.com/MobileUpLLC/FormView",
149-
.upToNextMajor(from: "1.1.2")
157+
.upToNextMajor(from: "1.3.0")
150158
)
151159
]
152160
```

Sources/FormView/FormField.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public struct FormField<Content: View>: View {
1313

1414
@State private var failedValidationRules: [ValidationRule] = []
1515

16+
private let isRequired: Bool
17+
private var isValid: Bool { getValidationStatus() }
18+
1619
// Fields Focus
1720
@FocusState private var isFocused: Bool
1821
@State private var id: String = UUID().uuidString
@@ -26,9 +29,11 @@ public struct FormField<Content: View>: View {
2629
public init(
2730
value: Binding<String>,
2831
rules: [ValidationRule] = [],
32+
isRequired: Bool,
2933
@ViewBuilder content: @escaping ([ValidationRule]) -> Content
3034
) {
3135
self._value = value
36+
self.isRequired = isRequired
3237
self.content = content
3338
self.validator = FieldValidator(rules: rules)
3439
}
@@ -57,6 +62,7 @@ public struct FormField<Content: View>: View {
5762
}
5863
]
5964
)
65+
.preference(key: FieldsValidationKey.self, value: [isValid])
6066
.focused($isFocused)
6167

6268
// Fields Validation
@@ -93,8 +99,8 @@ public struct FormField<Content: View>: View {
9399

94100
if
95101
validationBehaviour.contains(.onFieldFocus)
96-
&& failedValidationRules.isEmpty
97-
&& newValue == true
102+
&& failedValidationRules.isEmpty
103+
&& newValue == true
98104
{
99105
failedValidationRules = await validator.validate(
100106
value: value,
@@ -105,4 +111,8 @@ public struct FormField<Content: View>: View {
105111
}
106112
}
107113
}
114+
115+
private func getValidationStatus() -> Bool {
116+
isRequired ? failedValidationRules.isEmpty && value.isEmpty == false : failedValidationRules.isEmpty
117+
}
108118
}

Sources/FormView/FormView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ private class FormStateHandler: ObservableObject {
6363

6464
public struct FormView<Content: View>: View {
6565
@StateObject private var formStateHandler = FormStateHandler()
66+
@Binding private var isAllFieldValid: Bool
6667
@ViewBuilder private let content: (FormValidator) -> Content
6768

6869
private let errorHideBehaviour: ErrorHideBehaviour
@@ -71,11 +72,13 @@ public struct FormView<Content: View>: View {
7172
public init(
7273
validate: [ValidationBehaviour] = [.manual],
7374
hideError: ErrorHideBehaviour = .onValueChanged,
75+
isAllFieldValid: Binding<Bool> = .constant(true),
7476
@ViewBuilder content: @escaping (FormValidator) -> Content
7577
) {
7678
self.content = content
7779
self.validationBehaviour = validate
7880
self.errorHideBehaviour = hideError
81+
self._isAllFieldValid = isAllFieldValid
7982
}
8083

8184
public var body: some View {
@@ -85,6 +88,9 @@ public struct FormView<Content: View>: View {
8588
.onPreferenceChange(FieldStatesKey.self) { [weak formStateHandler] newStates in
8689
formStateHandler?.updateFieldStates(newStates: newStates)
8790
}
91+
.onPreferenceChange(FieldsValidationKey.self) { validationResults in
92+
isAllFieldValid = validationResults.contains(false) == false
93+
}
8894
.onSubmit(of: .text) { [weak formStateHandler] in
8995
formStateHandler?.submit()
9096
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// FieldsValidationKey.swift
3+
// FormView
4+
//
5+
// Created by Victor Kostin on 31.03.2025.
6+
//
7+
8+
import SwiftUI
9+
10+
struct FieldsValidationKey: PreferenceKey {
11+
static var defaultValue: [Bool] = []
12+
13+
static func reduce(value: inout [Bool], nextValue: () -> [Bool]) {
14+
value += nextValue()
15+
}
16+
}

0 commit comments

Comments
 (0)