diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index 345c031..9bfef4e 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -11,35 +11,42 @@ import FormView struct ContentView: View { @ObservedObject var viewModel: ContentViewModel + @State private var isAllFieldValid = false + var body: some View { FormView( validate: [.manual, .onFieldValueChanged, .onFieldFocus], - hideError: .onValueChanged + hideError: .onValueChanged, + isAllFieldValid: $isAllFieldValid ) { 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) } @@ -52,7 +59,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 ed033ef..0dbe2bc 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,18 @@ struct MyField: View { struct ContentView: View { @State var name: String = "" + @State private var isAllFieldValid = false + var body: some View { FormView( First failed field validate: [.manual], // 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, - rules: [ValidationRule.notEmpty(conditions: [.manual], 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) } @@ -73,6 +77,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 +97,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 Extend `ValidationRule`: @@ -146,7 +154,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..486c257 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -13,6 +13,9 @@ public struct FormField: View { @State private var failedValidationRules: [ValidationRule] = [] + private let isRequired: Bool + private var isValid: Bool { getValidationStatus() } + // Fields Focus @FocusState private var isFocused: Bool @State private var id: String = UUID().uuidString @@ -26,9 +29,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) } @@ -57,6 +62,7 @@ public struct FormField: View { } ] ) + .preference(key: FieldsValidationKey.self, value: [isValid]) .focused($isFocused) // Fields Validation @@ -93,8 +99,8 @@ public struct FormField: View { if validationBehaviour.contains(.onFieldFocus) - && failedValidationRules.isEmpty - && newValue == true + && failedValidationRules.isEmpty + && newValue == true { failedValidationRules = await validator.validate( value: value, @@ -105,4 +111,8 @@ public struct FormField: View { } } } + + private func getValidationStatus() -> Bool { + isRequired ? failedValidationRules.isEmpty && value.isEmpty == false : failedValidationRules.isEmpty + } } 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() + } +}