From 0f1e57fdac9e178cd2118c0fb74744f75e914115 Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Mon, 31 Mar 2025 14:58:39 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=90dd=20the=20ability=20to=20track=20val?= =?UTF-8?q?idation=20without=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UI/ContentScreen/ContentView.swift | 6 ++++-- README.md | 10 ++++++++-- Sources/FormView/FormField.swift | 2 ++ Sources/FormView/FormView.swift | 6 ++++++ .../Preference/FieldsValidationKey.swift | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 Sources/FormView/Preference/FieldsValidationKey.swift diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index 345c031..df23e8e 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -10,11 +10,13 @@ import FormView struct ContentView: View { @ObservedObject var viewModel: ContentViewModel + @State var isAllFieldValid = false var body: some View { FormView( validate: [.manual, .onFieldValueChanged, .onFieldFocus], - hideError: .onValueChanged + hideError: .onValueChanged, + isAllFieldValid: $isAllFieldValid ) { proxy in FormField( value: $viewModel.name, @@ -52,7 +54,7 @@ struct ContentView: View { print("Form is valid: \(await proxy.validate())") } } - .disabled(viewModel.isLoading) + .disabled(isAllFieldValid == false || viewModel.isLoading) } .padding(.horizontal, 16) .padding(.top, 40) diff --git a/README.md b/README.md index cf76625..e0b356d 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,13 @@ struct MyField: View { ```swift struct ContentView: View { @State var name: String = "" + @State var isAllFieldValid = false var body: some View { FormView( First failed field validate: .never, // Form will be validated on user action. - hideError: .onValueChanged // Error for field wil be hidden on field value change. + hideError: .onValueChanged, // Error for field wil be hidden on field value change. + isAllFieldValid: $isAllFieldValid // Property indicating the result of validation of all fields without focus ) { proxy in FormField( value: $name, @@ -74,6 +76,7 @@ struct ContentView: View { // Validate form on user action. print("Form is valid: \(proxy.validate())") } + .disabled(isAllFieldValid == false) // Use isAllFieldValid to automatically disable the action button } } } @@ -92,6 +95,9 @@ Error for each field gets hidden at one of three specific times: * `onFocus` - field with error is focused.. * `onFucusLost` - field with error lost focus. +### Is All Field Valid +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. + ### Custom Validation Rules One of two ways: 1. Adopt protocol `ValidationRule`: @@ -224,7 +230,7 @@ FormView doesn't use any external dependencies. dependencies: [ .package( url: "https://github.com/MobileUpLLC/FormView", - .upToNextMajor(from: "1.1.2") + .upToNextMajor(from: "1.3.0") ) ] ``` diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index 65f06be..c8de331 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -12,6 +12,7 @@ public struct FormField: View { @ViewBuilder private let content: ([ValidationRule]) -> Content @State private var failedValidationRules: [ValidationRule] = [] + private var isValid: Bool { failedValidationRules.isEmpty && value.isEmpty == false } // Fields Focus @FocusState private var isFocused: Bool @@ -57,6 +58,7 @@ public struct FormField: View { } ] ) + .preference(key: FieldsValidationKey.self, value: [isValid]) .focused($isFocused) // Fields Validation diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index a00c2d9..9c2cbde 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -63,6 +63,7 @@ private class FormStateHandler: ObservableObject { public struct FormView: View { @StateObject private var formStateHandler = FormStateHandler() + @Binding private var isAllFieldValid: Bool @ViewBuilder private let content: (FormValidator) -> Content private let errorHideBehaviour: ErrorHideBehaviour @@ -71,11 +72,13 @@ public struct FormView: View { public init( validate: [ValidationBehaviour] = [.manual], hideError: ErrorHideBehaviour = .onValueChanged, + isAllFieldValid: Binding = .constant(true), @ViewBuilder content: @escaping (FormValidator) -> Content ) { self.content = content self.validationBehaviour = validate self.errorHideBehaviour = hideError + self._isAllFieldValid = isAllFieldValid } public var body: some View { @@ -85,6 +88,9 @@ public struct FormView: View { .onPreferenceChange(FieldStatesKey.self) { [weak formStateHandler] newStates in formStateHandler?.updateFieldStates(newStates: newStates) } + .onPreferenceChange(FieldsValidationKey.self) { validationResults in + isAllFieldValid = validationResults.contains(false) == false + } .onSubmit(of: .text) { [weak formStateHandler] in formStateHandler?.submit() } diff --git a/Sources/FormView/Preference/FieldsValidationKey.swift b/Sources/FormView/Preference/FieldsValidationKey.swift new file mode 100644 index 0000000..eefed9f --- /dev/null +++ b/Sources/FormView/Preference/FieldsValidationKey.swift @@ -0,0 +1,16 @@ +// +// FieldsValidationKey.swift +// FormView +// +// Created by Victor Kostin on 31.03.2025. +// + +import SwiftUI + +struct FieldsValidationKey: PreferenceKey { + static var defaultValue: [Bool] = [] + + static func reduce(value: inout [Bool], nextValue: () -> [Bool]) { + value += nextValue() + } +} From 37305a3e53e1ec71d0c6064dc7be070eaac2071d Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Tue, 1 Apr 2025 13:33:01 +0300 Subject: [PATCH 2/3] Add isRequired logic --- .../ExampleApp/UI/ContentScreen/ContentView.swift | 12 ++++++++---- README.md | 3 ++- Sources/FormView/FormField.swift | 13 ++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index df23e8e..d5baa07 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -20,28 +20,32 @@ struct ContentView: View { ) { proxy in FormField( value: $viewModel.name, - rules: viewModel.nameValidationRules + rules: viewModel.nameValidationRules, + isRequired: true ) { failedRules in TextInputField(title: "Name", text: $viewModel.name, failedRules: failedRules) } .disabled(viewModel.isLoading) FormField( value: $viewModel.age, - rules: viewModel.ageValidationRules + rules: viewModel.ageValidationRules, + isRequired: false ) { failedRules in TextInputField(title: "Age", text: $viewModel.age, failedRules: failedRules) } .disabled(viewModel.isLoading) FormField( value: $viewModel.pass, - rules: viewModel.passValidationRules + rules: viewModel.passValidationRules, + isRequired: true ) { failedRules in SecureInputField(title: "Password", text: $viewModel.pass, failedRules: failedRules) } .disabled(viewModel.isLoading) FormField( value: $viewModel.confirmPass, - rules: viewModel.confirmPassValidationRules + rules: viewModel.confirmPassValidationRules, + isRequired: true ) { failedRules in SecureInputField(title: "Confirm Password", text: $viewModel.confirmPass, failedRules: failedRules) } diff --git a/README.md b/README.md index e0b356d..c0337b9 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ struct ContentView: View { ) { proxy in FormField( value: $name, - rules: [TextValidationRule.notEmpty(message: "Name field should no be empty")] + rules: [TextValidationRule.notEmpty(message: "Name field should no be empty")], + isRequired: true, // field parameter, necessary for correct determination of validity of all fields ) { failedRules in MyField(title: "Name", text: $name, failedRules: failedRules) } diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index c8de331..a222adb 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -12,7 +12,8 @@ public struct FormField: View { @ViewBuilder private let content: ([ValidationRule]) -> Content @State private var failedValidationRules: [ValidationRule] = [] - private var isValid: Bool { failedValidationRules.isEmpty && value.isEmpty == false } + private var isValid: Bool { getValidationStatus() } + private var isRequired: Bool // Fields Focus @FocusState private var isFocused: Bool @@ -27,9 +28,11 @@ public struct FormField: View { public init( value: Binding, rules: [ValidationRule] = [], + isRequired: Bool, @ViewBuilder content: @escaping ([ValidationRule]) -> Content ) { self._value = value + self.isRequired = isRequired self.content = content self.validator = FieldValidator(rules: rules) } @@ -95,8 +98,8 @@ public struct FormField: View { if validationBehaviour.contains(.onFieldFocus) - && failedValidationRules.isEmpty - && newValue == true + && failedValidationRules.isEmpty + && newValue == true { failedValidationRules = await validator.validate( value: value, @@ -107,4 +110,8 @@ public struct FormField: View { } } } + + private func getValidationStatus() -> Bool { + isRequired ? failedValidationRules.isEmpty && value.isEmpty == false : failedValidationRules.isEmpty + } } From 1fd9b96c4c45779067bb3f23306a1a27fec64259 Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Tue, 15 Apr 2025 11:55:10 +0300 Subject: [PATCH 3/3] Fix mr comment --- ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift | 3 ++- README.md | 3 ++- Sources/FormView/FormField.swift | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index d5baa07..9bfef4e 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -10,7 +10,8 @@ import FormView struct ContentView: View { @ObservedObject var viewModel: ContentViewModel - @State var isAllFieldValid = false + + @State private var isAllFieldValid = false var body: some View { FormView( diff --git a/README.md b/README.md index c0337b9..f86cc3b 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ struct MyField: View { ```swift struct ContentView: View { @State var name: String = "" - @State var isAllFieldValid = false + + @State private var isAllFieldValid = false var body: some View { FormView( First failed field diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index a222adb..486c257 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -12,8 +12,9 @@ public struct FormField: View { @ViewBuilder private let content: ([ValidationRule]) -> Content @State private var failedValidationRules: [ValidationRule] = [] + + private let isRequired: Bool private var isValid: Bool { getValidationStatus() } - private var isRequired: Bool // Fields Focus @FocusState private var isFocused: Bool