@@ -36,16 +36,15 @@ struct MyField: View {
3636 let title: String
3737 let text: Binding<String >
3838 // Failed rules come from FormField during whole form validation.
39- let failedRules: [TextValidationRule ]
39+ let failedRules: [ValidationRule ]
4040
4141 var body: some View {
4242 VStack (alignment : .leading ) {
4343 TextField (title, text : text)
4444 .background (Color.white )
4545 // Display error.
46- if failedRules.isEmpty == false {
47- // Show error for first failed rule.
48- Text (failedRules[0 ].errorMessage )
46+ if let errorMessage = failedRules.first ? .message, errorMessage.isEmpty == false {
47+ Text (errorMessage)
4948 .foregroundColor (.red )
5049 }
5150 }
@@ -60,12 +59,12 @@ struct ContentView: View {
6059
6160 var body: some View {
6261 FormView ( First failed field
63- validate: . never , // Form will be validated on user action.
62+ validate: [. manual ] , // Form will be validated on user action.
6463 hideError : .onValueChanged // Error for field wil be hidden on field value change.
6564 ) { proxy in
6665 FormField (
6766 value : $name,
68- rules : [TextValidationRule .notEmpty (message : " Name field should no be empty" )]
67+ rules : [ValidationRule .notEmpty (conditions : [. manual ], message : " Name field should no be empty" )]
6968 ) { failedRules in
7069 MyField (title : " Name" , text : $name, failedRules : failedRules)
7170 }
@@ -81,10 +80,11 @@ struct ContentView: View {
8180
8281## Documentation
8382### Valildation Behaviour
84- Form validated at one of three specific times :
83+ The form can be validated under one or multiple conditions simultaneously, such as :
8584* ` onFieldValueChanged ` - each field validated on it's value changed.
85+ * ` onFieldFocus ` - each field validated on focus gain.
8686* ` onFieldFocusLost ` - each field validated on focus lost.
87- * ` never ` - on call ` proxy.validate() ` . Default behaviour. First failed field is focused automatically.
87+ * ` manual ` - on call ` proxy.validate() ` . Default behaviour. First failed field is focused automatically.
8888
8989### Error Hiding Behaviour
9090Error for each field gets hidden at one of three specific times:
@@ -93,123 +93,45 @@ Error for each field gets hidden at one of three specific times:
9393* ` onFucusLost ` - field with error lost focus.
9494
9595### Custom Validation Rules
96- One of two ways:
97- 1 . Adopt protocol ` ValidationRule ` :
98- ``` swift
99- public protocol ValidationRule {
100- associatedtype Value
101-
102- func check (value : Value ) -> Bool
103- }
104- ```
10596
106- 2 . Extend ` TextValidationRule ` :
97+ Extend ` ValidationRule ` :
10798``` swift
108- extension TextValidationRule {
99+ extension ValidationRule {
109100 static var myRule: Self {
110- TextValidationRule ( message : " Text should not be empty " ) { value in
111- value .isEmpty == false
101+ Self . custom ( conditions : [. manual , . onFieldValueChanged , . onFieldFocus ] ) {
102+ return ( $0 .isEmpty == false , " Text should not be empty " )
112103 }
113104 }
114105}
115106```
116107
117- A banch of predefind rules for text validation is available via ` TextValidationRule ` :
108+ A banch of predefind rules for text validation is available via ` ValidationRule ` :
118109* notEmpty - value not empty.
119110* digitsOnly - value contains only digits.
120111* lettersOnly - value contains only letters.
121112* email - is valid email.
122113* minLenght/maxLenth - value length greate/less.
123114* regex - evaluate regular expresstion.
124- * equalTo - value equal to another value. Useful for password confirmation.
125115* etc...
126116
127- ### Outer Validation Rules
128- If you need to display validation errors from external services (e.g., a backend), follow these steps:
129- 1 . Create an ` OuterValidationRule ` enum:
117+ ### External Validation Rules
118+ If you need to display validation errors from external services (e.g., a backend) use ` ValidationRule.external ` :
130119``` swift
131- enum OuterValidationRule {
132- case duplicateName
133-
134- var message: String {
135- switch self {
136- case .duplicateName :
137- return " This name already exists"
138- }
120+ ValidationRule.external { [weak self ] in
121+ guard let self else {
122+ return (true , " " )
139123 }
140- }
141- ```
142124
143- 2 . Update the text field component:
144- ``` swift
145- struct TextInputField : View {
146- let title: LocalizedStringKey
147- @Binding var text: String
148- let failedRules: [TextValidationRule]
149- @Binding var outerRules: [OuterValidationRule]
150-
151- var body: some View {
152- VStack (alignment : .leading ) {
153- TextField (title, text : $text)
154- .background (Color.white )
155- if let errorMessage = getErrorMessage () {
156- Text (errorMessage)
157- .font (.system (size : 12 , weight : .semibold ))
158- .foregroundColor (.red )
159- }
160- Spacer ()
161- }
162- .frame (height : 50 )
163- .onChange (of : text) { _ in
164- outerRules = []
165- }
166- }
167-
168- private func getErrorMessage () -> String ? {
169- if let message = failedRules.first ? .message {
170- return message
171- } else if let message = outerRules.first ? .message {
172- return message
173- } else {
174- return nil
175- }
176- }
177-
178- init (
179- title : LocalizedStringKey,
180- text : Binding<String >,
181- failedRules : [TextValidationRule],
182- outerRules : Binding<[OuterValidationRule]> = .constant ([])
183- ) {
184- self .title = title
185- self ._text = text
186- self .failedRules = failedRules
187- self ._outerRules = outerRules
188- }
125+ return await self .availabilityCheckAsync ($0 )
189126}
190- ```
191- 3 . Update the text field initialization in your view:
192- ``` swift
193- TextInputField (
194- title : " Name" ,
195- text : $viewModel.name ,
196- failedRules : failedRules,
197- outerRules : $viewModel.nameOuterRules
198- )
199- ```
200127
201- 4 . In your ViewModel, declare a ` @Published ` property of type ` OuterValidationRule ` and update its rules as needed:
202- ``` swift
203- class ContentViewModel : ObservableObject {
204- @Published var nameOuterRules: [OuterValidationRule] = []
205-
206- func applyNameOuterRules () {
207- nameOuterRules = [.duplicateName ]
208- }
128+ private func availabilityCheckAsync (_ value : String ) async -> (Bool , String ) {
129+ let isAvailable = try await ...
130+
131+ return (isAvailable, " Not available" )
209132}
210133```
211134
212-
213135### Implementation Details
214136FormView doesn't use any external dependencies.
215137
0 commit comments