diff --git a/ExampleApp/ExampleApp/InputFields/SecureInputField.swift b/ExampleApp/ExampleApp/InputFields/SecureInputField.swift index 9ceddc3..bb2d6fe 100644 --- a/ExampleApp/ExampleApp/InputFields/SecureInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/SecureInputField.swift @@ -24,8 +24,8 @@ struct SecureInputField: View { eyeImage } .background(Color.white) - if failedRules.isEmpty == false, let message = failedRules[0].message { - Text(message) + if failedRules.isEmpty == false, failedRules[0].message.isEmpty == false { + Text(failedRules[0].message) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.red) } diff --git a/ExampleApp/ExampleApp/InputFields/TextInputField.swift b/ExampleApp/ExampleApp/InputFields/TextInputField.swift index d39d1a4..0f47e55 100644 --- a/ExampleApp/ExampleApp/InputFields/TextInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/TextInputField.swift @@ -17,7 +17,7 @@ struct TextInputField: View { VStack(alignment: .leading) { TextField(title, text: $text) .background(Color.white) - if let errorMessage = failedRules.first?.message { + if let errorMessage = failedRules.first?.message, errorMessage.isEmpty == false { Text(errorMessage) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.red) diff --git a/ExampleApp/ExampleApp/MyRule.swift b/ExampleApp/ExampleApp/MyRule.swift index 1372ce1..16ca586 100644 --- a/ExampleApp/ExampleApp/MyRule.swift +++ b/ExampleApp/ExampleApp/MyRule.swift @@ -8,8 +8,8 @@ import FormView extension ValidationRule { static var myRule: Self { - Self.custom { - return $0.contains("T") ? nil : "Should contain T" + Self.custom(conditions: [.manual, .onFieldValueChanged, .onFieldFocus]) { + return ($0.contains("T"), "Should contain T") } } } diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index 9e0427c..345c031 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -13,7 +13,7 @@ struct ContentView: View { var body: some View { FormView( - validate: .onFieldValueChanged, + validate: [.manual, .onFieldValueChanged, .onFieldFocus], hideError: .onValueChanged ) { proxy in FormField( diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift index fcce647..6e587e0 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift @@ -31,33 +31,42 @@ class ContentViewModel: ObservableObject { private func setupValidationRules() { nameValidationRules = [ - ValidationRule.notEmpty(message: "Name empty"), - ValidationRule.noSpecialCharacters(message: "No spec chars"), + ValidationRule.notEmpty(conditions: [.manual, .onFieldValueChanged, .onFieldFocus], message: "Name empty"), + ValidationRule.noSpecialCharacters( + conditions: [.manual, .onFieldValueChanged, .onFieldFocus], + message: "No spec chars" + ), ValidationRule.myRule, - ValidationRule.external { [weak self] in await self?.availabilityCheckAsync($0) } + ValidationRule.external { [weak self] in + guard let self else { + return (true, "") + } + + return await self.availabilityCheckAsync($0) + } ] ageValidationRules = [ - ValidationRule.digitsOnly(message: "Digits only"), - ValidationRule.maxLength(count: 2, message: "Max length 2") + ValidationRule.digitsOnly(conditions: [.manual, .onFieldValueChanged], message: "Digits only"), + ValidationRule.maxLength(conditions: [.manual, .onFieldValueChanged], count: 2, message: "Max length 2") ] passValidationRules = [ - ValidationRule.atLeastOneDigit(message: "One digit"), - ValidationRule.atLeastOneLetter(message: "One letter"), - ValidationRule.notEmpty(message: "Pass not empty") + ValidationRule.atLeastOneDigit(conditions: [.manual, .onFieldValueChanged], message: "One digit"), + ValidationRule.atLeastOneLetter(conditions: [.manual, .onFieldValueChanged], message: "One letter"), + ValidationRule.notEmpty(conditions: [.manual, .onFieldValueChanged], message: "Pass not empty") ] confirmPassValidationRules = [ - ValidationRule.notEmpty(message: "Confirm pass not empty"), - ValidationRule.custom { [weak self] in - return $0 == self?.pass ? nil : "Not equal to pass" + ValidationRule.notEmpty(conditions: [.manual, .onFieldValueChanged], message: "Confirm pass not empty"), + ValidationRule.custom(conditions: [.manual, .onFieldValueChanged]) { [weak self] in + return ($0 == self?.pass, "Not equal to pass") } ] } @MainActor - private func availabilityCheckAsync(_ value: String) async -> String? { + private func availabilityCheckAsync(_ value: String) async -> (Bool, String) { print(#function) isLoading = true @@ -68,7 +77,7 @@ class ContentViewModel: ObservableObject { isLoading = false - return isAvailable ? nil : "Not available" + return (isAvailable, "Not available") } deinit { diff --git a/Sources/FormView/Environment/EnvironmentKeys.swift b/Sources/FormView/Environment/EnvironmentKeys.swift index 61b91fe..141bedc 100644 --- a/Sources/FormView/Environment/EnvironmentKeys.swift +++ b/Sources/FormView/Environment/EnvironmentKeys.swift @@ -23,11 +23,11 @@ extension EnvironmentValues { // MARK: - ValidationBehaviourKey private struct ValidationBehaviourKey: EnvironmentKey { - static var defaultValue: ValidationBehaviour = .never + static var defaultValue: [ValidationBehaviour] = [.manual] } extension EnvironmentValues { - var validationBehaviour: ValidationBehaviour { + var validationBehaviour: [ValidationBehaviour] { get { self[ValidationBehaviourKey.self] } set { self[ValidationBehaviourKey.self] = newValue } } diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index 0fa9486..65f06be 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -46,7 +46,11 @@ public struct FormField: View { value: [ // Замыкание для каждого филда вызывается FormValidator'ом из FormView для валидации по требованию FieldState(id: id, isFocused: isFocused) { - let failedRules = await validator.validate(value: value, isNeedToCheckExternal: true) + let failedRules = await validator.validate( + value: value, + condition: .manual, + isNeedToCheckExternal: true + ) failedValidationRules = failedRules return failedRules.isEmpty @@ -62,8 +66,12 @@ public struct FormField: View { failedValidationRules = .empty } - if validationBehaviour == .onFieldValueChanged { - failedValidationRules = await validator.validate(value: newValue, isNeedToCheckExternal: false) + if validationBehaviour.contains(.onFieldValueChanged) { + failedValidationRules = await validator.validate( + value: newValue, + condition: .onFieldValueChanged, + isNeedToCheckExternal: false + ) } } } @@ -75,8 +83,24 @@ public struct FormField: View { failedValidationRules = .empty } - if validationBehaviour == .onFieldFocusLost && newValue == false { - failedValidationRules = await validator.validate(value: value, isNeedToCheckExternal: false) + if validationBehaviour.contains(.onFieldFocusLost) && newValue == false { + failedValidationRules = await validator.validate( + value: value, + condition: .onFieldFocusLost, + isNeedToCheckExternal: false + ) + } + + if + validationBehaviour.contains(.onFieldFocus) + && failedValidationRules.isEmpty + && newValue == true + { + failedValidationRules = await validator.validate( + value: value, + condition: .onFieldFocus, + isNeedToCheckExternal: false + ) } } } diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index a33fa83..a00c2d9 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -9,8 +9,9 @@ import SwiftUI public enum ValidationBehaviour { case onFieldValueChanged + case onFieldFocus case onFieldFocusLost - case never + case manual } public enum ErrorHideBehaviour { @@ -65,10 +66,10 @@ public struct FormView: View { @ViewBuilder private let content: (FormValidator) -> Content private let errorHideBehaviour: ErrorHideBehaviour - private let validationBehaviour: ValidationBehaviour + private let validationBehaviour: [ValidationBehaviour] public init( - validate: ValidationBehaviour = .never, + validate: [ValidationBehaviour] = [.manual], hideError: ErrorHideBehaviour = .onValueChanged, @ViewBuilder content: @escaping (FormValidator) -> Content ) { diff --git a/Sources/FormView/Validation/Rules/ValidationRule.swift b/Sources/FormView/Validation/Rules/ValidationRule.swift index 5f67b7f..0346864 100644 --- a/Sources/FormView/Validation/Rules/ValidationRule.swift +++ b/Sources/FormView/Validation/Rules/ValidationRule.swift @@ -9,116 +9,128 @@ import Foundation import SwiftUI public class ValidationRule { - public var message: String? + public var message: String public let isExternal: Bool + public let conditions: [ValidationBehaviour] - private let checkClosure: (String) async -> String? + private let checkClosure: (String) async -> (Bool, String) - internal required init(isExternal: Bool, checkClosure: @escaping (String) async -> String?) { + internal required init( + conditions: [ValidationBehaviour], + isExternal: Bool, + checkClosure: @escaping (String) async -> (Bool, String) + ) { + self.message = .empty self.checkClosure = checkClosure self.isExternal = isExternal + self.conditions = conditions } public func check(value: String) async -> Bool { - let message = await checkClosure(value) + let (result, message) = await checkClosure(value) self.message = message - return message == nil + return result } } extension ValidationRule { - public static func custom(checkClosure: @escaping (String) async -> String?) -> Self { - return Self(isExternal: false, checkClosure: checkClosure) + public static func custom( + conditions: [ValidationBehaviour], + checkClosure: @escaping (String) async -> (Bool, String) + ) -> Self { + return Self(conditions: conditions, isExternal: false, checkClosure: checkClosure) } - public static func external(checkClosure: @escaping (String) async -> String?) -> Self { - return Self(isExternal: true, checkClosure: checkClosure) + public static func external(checkClosure: @escaping (String) async -> (Bool, String)) -> Self { + return Self(conditions: [.manual], isExternal: true, checkClosure: checkClosure) } - public static func notEmpty(message: String) -> Self { - return Self(isExternal: false) { - return $0.isEmpty == false ? nil : message + public static func notEmpty(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.isEmpty == false, message) } } - public static func atLeastOneLowercaseLetter(message: String) -> Self { - return Self(isExternal: false) { - return $0.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil ? nil : message + public static func atLeastOneLowercaseLetter(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil, message) } } - public static func atLeastOneUppercaseLetter(message: String) -> Self { - return Self(isExternal: false) { - $0.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil ? nil : message + public static func atLeastOneUppercaseLetter(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil, message) } } - public static func atLeastOneDigit(message: String) -> Self { - return Self(isExternal: false) { - $0.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil ? nil : message + public static func atLeastOneDigit(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil, message) } } - public static func atLeastOneLetter(message: String) -> Self { - return Self(isExternal: false) { - $0.rangeOfCharacter(from: CharacterSet.letters) != nil ? nil : message + public static func atLeastOneLetter(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.rangeOfCharacter(from: CharacterSet.letters) != nil, message) } } - public static func digitsOnly(message: String) -> Self { - return Self(isExternal: false) { - CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: $0)) ? nil : message + public static func digitsOnly(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return (CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: $0)), message) } } - public static func lettersOnly(message: String) -> Self { - return Self(isExternal: false) { - CharacterSet.letters.isSuperset(of: CharacterSet(charactersIn: $0)) ? nil : message + public static func lettersOnly(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return (CharacterSet.letters.isSuperset(of: CharacterSet(charactersIn: $0)), message) } } - public static func atLeastOneSpecialCharacter(message: String) -> Self { - return Self(isExternal: false) { - $0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) != nil ? nil : message + public static func atLeastOneSpecialCharacter(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) != nil, message) } } - public static func noSpecialCharacters(message: String) -> Self { - return Self(isExternal: false) { - $0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) == nil ? nil : message + public static func noSpecialCharacters(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) == nil, message) } } - public static func email(message: String) -> Self { - return Self(isExternal: false) { - NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}") - .evaluate(with: $0) ? nil : message + public static func email(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ( + NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}") + .evaluate(with: $0), + message + ) } } - public static func notRecurringPincode(message: String) -> Self { - return Self(isExternal: false) { - $0.range(of: "([0-9])\\1\\1\\1", options: .regularExpression) == nil ? nil : message + public static func notRecurringPincode(conditions: [ValidationBehaviour], message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.range(of: "([0-9])\\1\\1\\1", options: .regularExpression) == nil, message) } } - public static func minLength(count: Int, message: String) -> Self { - return Self(isExternal: false) { - $0.count >= count ? nil : message + public static func minLength(conditions: [ValidationBehaviour], count: Int, message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.count >= count, message) } } - public static func maxLength(count: Int, message: String) -> Self { - return Self(isExternal: false) { - $0.count <= count ? nil : message + public static func maxLength(conditions: [ValidationBehaviour], count: Int, message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return ($0.count <= count, message) } } - public static func regex(value: String, message: String) -> Self { - return Self(isExternal: false) { - NSPredicate(format: "SELF MATCHES %@", value) - .evaluate(with: $0) ? nil : message + public static func regex(conditions: [ValidationBehaviour], value: String, message: String) -> Self { + return Self(conditions: conditions, isExternal: false) { + return (NSPredicate(format: "SELF MATCHES %@", value).evaluate(with: $0), message) } } } diff --git a/Sources/FormView/Validation/Validators/FieldValidator.swift b/Sources/FormView/Validation/Validators/FieldValidator.swift index ec7d609..6acaf40 100644 --- a/Sources/FormView/Validation/Validators/FieldValidator.swift +++ b/Sources/FormView/Validation/Validators/FieldValidator.swift @@ -14,10 +14,15 @@ struct FieldValidator { self.rules = rules } - func validate(value: String, isNeedToCheckExternal: Bool) async -> [ValidationRule] { + func validate( + value: String, + condition: ValidationBehaviour, + isNeedToCheckExternal: Bool + ) async -> [ValidationRule] { var failedRules: [ValidationRule] = [] - for rule in rules where rule.isExternal == false || isNeedToCheckExternal { + for rule in rules where (rule.isExternal == false || isNeedToCheckExternal) + && rule.conditions.contains(condition) { if await rule.check(value: value) == false { failedRules.append(rule) }