Skip to content

Commit 05f63a4

Browse files
authored
UPUP-1008
1 parent 894bfd3 commit 05f63a4

10 files changed

Lines changed: 135 additions & 84 deletions

File tree

ExampleApp/ExampleApp/InputFields/SecureInputField.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ struct SecureInputField: View {
2424
eyeImage
2525
}
2626
.background(Color.white)
27-
if failedRules.isEmpty == false, let message = failedRules[0].message {
28-
Text(message)
27+
if failedRules.isEmpty == false, failedRules[0].message.isEmpty == false {
28+
Text(failedRules[0].message)
2929
.font(.system(size: 12, weight: .semibold))
3030
.foregroundColor(.red)
3131
}

ExampleApp/ExampleApp/InputFields/TextInputField.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct TextInputField: View {
1717
VStack(alignment: .leading) {
1818
TextField(title, text: $text)
1919
.background(Color.white)
20-
if let errorMessage = failedRules.first?.message {
20+
if let errorMessage = failedRules.first?.message, errorMessage.isEmpty == false {
2121
Text(errorMessage)
2222
.font(.system(size: 12, weight: .semibold))
2323
.foregroundColor(.red)

ExampleApp/ExampleApp/MyRule.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import FormView
88

99
extension ValidationRule {
1010
static var myRule: Self {
11-
Self.custom {
12-
return $0.contains("T") ? nil : "Should contain T"
11+
Self.custom(conditions: [.manual, .onFieldValueChanged, .onFieldFocus]) {
12+
return ($0.contains("T"), "Should contain T")
1313
}
1414
}
1515
}

ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ struct ContentView: View {
1313

1414
var body: some View {
1515
FormView(
16-
validate: .onFieldValueChanged,
16+
validate: [.manual, .onFieldValueChanged, .onFieldFocus],
1717
hideError: .onValueChanged
1818
) { proxy in
1919
FormField(

ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,42 @@ class ContentViewModel: ObservableObject {
3131

3232
private func setupValidationRules() {
3333
nameValidationRules = [
34-
ValidationRule.notEmpty(message: "Name empty"),
35-
ValidationRule.noSpecialCharacters(message: "No spec chars"),
34+
ValidationRule.notEmpty(conditions: [.manual, .onFieldValueChanged, .onFieldFocus], message: "Name empty"),
35+
ValidationRule.noSpecialCharacters(
36+
conditions: [.manual, .onFieldValueChanged, .onFieldFocus],
37+
message: "No spec chars"
38+
),
3639
ValidationRule.myRule,
37-
ValidationRule.external { [weak self] in await self?.availabilityCheckAsync($0) }
40+
ValidationRule.external { [weak self] in
41+
guard let self else {
42+
return (true, "")
43+
}
44+
45+
return await self.availabilityCheckAsync($0)
46+
}
3847
]
3948

4049
ageValidationRules = [
41-
ValidationRule.digitsOnly(message: "Digits only"),
42-
ValidationRule.maxLength(count: 2, message: "Max length 2")
50+
ValidationRule.digitsOnly(conditions: [.manual, .onFieldValueChanged], message: "Digits only"),
51+
ValidationRule.maxLength(conditions: [.manual, .onFieldValueChanged], count: 2, message: "Max length 2")
4352
]
4453

4554
passValidationRules = [
46-
ValidationRule.atLeastOneDigit(message: "One digit"),
47-
ValidationRule.atLeastOneLetter(message: "One letter"),
48-
ValidationRule.notEmpty(message: "Pass not empty")
55+
ValidationRule.atLeastOneDigit(conditions: [.manual, .onFieldValueChanged], message: "One digit"),
56+
ValidationRule.atLeastOneLetter(conditions: [.manual, .onFieldValueChanged], message: "One letter"),
57+
ValidationRule.notEmpty(conditions: [.manual, .onFieldValueChanged], message: "Pass not empty")
4958
]
5059

5160
confirmPassValidationRules = [
52-
ValidationRule.notEmpty(message: "Confirm pass not empty"),
53-
ValidationRule.custom { [weak self] in
54-
return $0 == self?.pass ? nil : "Not equal to pass"
61+
ValidationRule.notEmpty(conditions: [.manual, .onFieldValueChanged], message: "Confirm pass not empty"),
62+
ValidationRule.custom(conditions: [.manual, .onFieldValueChanged]) { [weak self] in
63+
return ($0 == self?.pass, "Not equal to pass")
5564
}
5665
]
5766
}
5867

5968
@MainActor
60-
private func availabilityCheckAsync(_ value: String) async -> String? {
69+
private func availabilityCheckAsync(_ value: String) async -> (Bool, String) {
6170
print(#function)
6271

6372
isLoading = true
@@ -68,7 +77,7 @@ class ContentViewModel: ObservableObject {
6877

6978
isLoading = false
7079

71-
return isAvailable ? nil : "Not available"
80+
return (isAvailable, "Not available")
7281
}
7382

7483
deinit {

Sources/FormView/Environment/EnvironmentKeys.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ extension EnvironmentValues {
2323
// MARK: - ValidationBehaviourKey
2424

2525
private struct ValidationBehaviourKey: EnvironmentKey {
26-
static var defaultValue: ValidationBehaviour = .never
26+
static var defaultValue: [ValidationBehaviour] = [.manual]
2727
}
2828

2929
extension EnvironmentValues {
30-
var validationBehaviour: ValidationBehaviour {
30+
var validationBehaviour: [ValidationBehaviour] {
3131
get { self[ValidationBehaviourKey.self] }
3232
set { self[ValidationBehaviourKey.self] = newValue }
3333
}

Sources/FormView/FormField.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ public struct FormField<Content: View>: View {
4646
value: [
4747
// Замыкание для каждого филда вызывается FormValidator'ом из FormView для валидации по требованию
4848
FieldState(id: id, isFocused: isFocused) {
49-
let failedRules = await validator.validate(value: value, isNeedToCheckExternal: true)
49+
let failedRules = await validator.validate(
50+
value: value,
51+
condition: .manual,
52+
isNeedToCheckExternal: true
53+
)
5054
failedValidationRules = failedRules
5155

5256
return failedRules.isEmpty
@@ -62,8 +66,12 @@ public struct FormField<Content: View>: View {
6266
failedValidationRules = .empty
6367
}
6468

65-
if validationBehaviour == .onFieldValueChanged {
66-
failedValidationRules = await validator.validate(value: newValue, isNeedToCheckExternal: false)
69+
if validationBehaviour.contains(.onFieldValueChanged) {
70+
failedValidationRules = await validator.validate(
71+
value: newValue,
72+
condition: .onFieldValueChanged,
73+
isNeedToCheckExternal: false
74+
)
6775
}
6876
}
6977
}
@@ -75,8 +83,24 @@ public struct FormField<Content: View>: View {
7583
failedValidationRules = .empty
7684
}
7785

78-
if validationBehaviour == .onFieldFocusLost && newValue == false {
79-
failedValidationRules = await validator.validate(value: value, isNeedToCheckExternal: false)
86+
if validationBehaviour.contains(.onFieldFocusLost) && newValue == false {
87+
failedValidationRules = await validator.validate(
88+
value: value,
89+
condition: .onFieldFocusLost,
90+
isNeedToCheckExternal: false
91+
)
92+
}
93+
94+
if
95+
validationBehaviour.contains(.onFieldFocus)
96+
&& failedValidationRules.isEmpty
97+
&& newValue == true
98+
{
99+
failedValidationRules = await validator.validate(
100+
value: value,
101+
condition: .onFieldFocus,
102+
isNeedToCheckExternal: false
103+
)
80104
}
81105
}
82106
}

Sources/FormView/FormView.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import SwiftUI
99

1010
public enum ValidationBehaviour {
1111
case onFieldValueChanged
12+
case onFieldFocus
1213
case onFieldFocusLost
13-
case never
14+
case manual
1415
}
1516

1617
public enum ErrorHideBehaviour {
@@ -65,10 +66,10 @@ public struct FormView<Content: View>: View {
6566
@ViewBuilder private let content: (FormValidator) -> Content
6667

6768
private let errorHideBehaviour: ErrorHideBehaviour
68-
private let validationBehaviour: ValidationBehaviour
69+
private let validationBehaviour: [ValidationBehaviour]
6970

7071
public init(
71-
validate: ValidationBehaviour = .never,
72+
validate: [ValidationBehaviour] = [.manual],
7273
hideError: ErrorHideBehaviour = .onValueChanged,
7374
@ViewBuilder content: @escaping (FormValidator) -> Content
7475
) {

Sources/FormView/Validation/Rules/ValidationRule.swift

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,116 +9,128 @@ import Foundation
99
import SwiftUI
1010

1111
public class ValidationRule {
12-
public var message: String?
12+
public var message: String
1313
public let isExternal: Bool
14+
public let conditions: [ValidationBehaviour]
1415

15-
private let checkClosure: (String) async -> String?
16+
private let checkClosure: (String) async -> (Bool, String)
1617

17-
internal required init(isExternal: Bool, checkClosure: @escaping (String) async -> String?) {
18+
internal required init(
19+
conditions: [ValidationBehaviour],
20+
isExternal: Bool,
21+
checkClosure: @escaping (String) async -> (Bool, String)
22+
) {
23+
self.message = .empty
1824
self.checkClosure = checkClosure
1925
self.isExternal = isExternal
26+
self.conditions = conditions
2027
}
2128

2229
public func check(value: String) async -> Bool {
23-
let message = await checkClosure(value)
30+
let (result, message) = await checkClosure(value)
2431
self.message = message
2532

26-
return message == nil
33+
return result
2734
}
2835
}
2936

3037
extension ValidationRule {
31-
public static func custom(checkClosure: @escaping (String) async -> String?) -> Self {
32-
return Self(isExternal: false, checkClosure: checkClosure)
38+
public static func custom(
39+
conditions: [ValidationBehaviour],
40+
checkClosure: @escaping (String) async -> (Bool, String)
41+
) -> Self {
42+
return Self(conditions: conditions, isExternal: false, checkClosure: checkClosure)
3343
}
3444

35-
public static func external(checkClosure: @escaping (String) async -> String?) -> Self {
36-
return Self(isExternal: true, checkClosure: checkClosure)
45+
public static func external(checkClosure: @escaping (String) async -> (Bool, String)) -> Self {
46+
return Self(conditions: [.manual], isExternal: true, checkClosure: checkClosure)
3747
}
3848

39-
public static func notEmpty(message: String) -> Self {
40-
return Self(isExternal: false) {
41-
return $0.isEmpty == false ? nil : message
49+
public static func notEmpty(conditions: [ValidationBehaviour], message: String) -> Self {
50+
return Self(conditions: conditions, isExternal: false) {
51+
return ($0.isEmpty == false, message)
4252
}
4353
}
4454

45-
public static func atLeastOneLowercaseLetter(message: String) -> Self {
46-
return Self(isExternal: false) {
47-
return $0.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil ? nil : message
55+
public static func atLeastOneLowercaseLetter(conditions: [ValidationBehaviour], message: String) -> Self {
56+
return Self(conditions: conditions, isExternal: false) {
57+
return ($0.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil, message)
4858
}
4959
}
5060

51-
public static func atLeastOneUppercaseLetter(message: String) -> Self {
52-
return Self(isExternal: false) {
53-
$0.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil ? nil : message
61+
public static func atLeastOneUppercaseLetter(conditions: [ValidationBehaviour], message: String) -> Self {
62+
return Self(conditions: conditions, isExternal: false) {
63+
return ($0.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil, message)
5464
}
5565
}
5666

57-
public static func atLeastOneDigit(message: String) -> Self {
58-
return Self(isExternal: false) {
59-
$0.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil ? nil : message
67+
public static func atLeastOneDigit(conditions: [ValidationBehaviour], message: String) -> Self {
68+
return Self(conditions: conditions, isExternal: false) {
69+
return ($0.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil, message)
6070
}
6171
}
6272

63-
public static func atLeastOneLetter(message: String) -> Self {
64-
return Self(isExternal: false) {
65-
$0.rangeOfCharacter(from: CharacterSet.letters) != nil ? nil : message
73+
public static func atLeastOneLetter(conditions: [ValidationBehaviour], message: String) -> Self {
74+
return Self(conditions: conditions, isExternal: false) {
75+
return ($0.rangeOfCharacter(from: CharacterSet.letters) != nil, message)
6676
}
6777
}
6878

69-
public static func digitsOnly(message: String) -> Self {
70-
return Self(isExternal: false) {
71-
CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: $0)) ? nil : message
79+
public static func digitsOnly(conditions: [ValidationBehaviour], message: String) -> Self {
80+
return Self(conditions: conditions, isExternal: false) {
81+
return (CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: $0)), message)
7282
}
7383
}
7484

75-
public static func lettersOnly(message: String) -> Self {
76-
return Self(isExternal: false) {
77-
CharacterSet.letters.isSuperset(of: CharacterSet(charactersIn: $0)) ? nil : message
85+
public static func lettersOnly(conditions: [ValidationBehaviour], message: String) -> Self {
86+
return Self(conditions: conditions, isExternal: false) {
87+
return (CharacterSet.letters.isSuperset(of: CharacterSet(charactersIn: $0)), message)
7888
}
7989
}
8090

81-
public static func atLeastOneSpecialCharacter(message: String) -> Self {
82-
return Self(isExternal: false) {
83-
$0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) != nil ? nil : message
91+
public static func atLeastOneSpecialCharacter(conditions: [ValidationBehaviour], message: String) -> Self {
92+
return Self(conditions: conditions, isExternal: false) {
93+
return ($0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) != nil, message)
8494
}
8595
}
8696

87-
public static func noSpecialCharacters(message: String) -> Self {
88-
return Self(isExternal: false) {
89-
$0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) == nil ? nil : message
97+
public static func noSpecialCharacters(conditions: [ValidationBehaviour], message: String) -> Self {
98+
return Self(conditions: conditions, isExternal: false) {
99+
return ($0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) == nil, message)
90100
}
91101
}
92102

93-
public static func email(message: String) -> Self {
94-
return Self(isExternal: false) {
95-
NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}")
96-
.evaluate(with: $0) ? nil : message
103+
public static func email(conditions: [ValidationBehaviour], message: String) -> Self {
104+
return Self(conditions: conditions, isExternal: false) {
105+
return (
106+
NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}")
107+
.evaluate(with: $0),
108+
message
109+
)
97110
}
98111
}
99112

100-
public static func notRecurringPincode(message: String) -> Self {
101-
return Self(isExternal: false) {
102-
$0.range(of: "([0-9])\\1\\1\\1", options: .regularExpression) == nil ? nil : message
113+
public static func notRecurringPincode(conditions: [ValidationBehaviour], message: String) -> Self {
114+
return Self(conditions: conditions, isExternal: false) {
115+
return ($0.range(of: "([0-9])\\1\\1\\1", options: .regularExpression) == nil, message)
103116
}
104117
}
105118

106-
public static func minLength(count: Int, message: String) -> Self {
107-
return Self(isExternal: false) {
108-
$0.count >= count ? nil : message
119+
public static func minLength(conditions: [ValidationBehaviour], count: Int, message: String) -> Self {
120+
return Self(conditions: conditions, isExternal: false) {
121+
return ($0.count >= count, message)
109122
}
110123
}
111124

112-
public static func maxLength(count: Int, message: String) -> Self {
113-
return Self(isExternal: false) {
114-
$0.count <= count ? nil : message
125+
public static func maxLength(conditions: [ValidationBehaviour], count: Int, message: String) -> Self {
126+
return Self(conditions: conditions, isExternal: false) {
127+
return ($0.count <= count, message)
115128
}
116129
}
117130

118-
public static func regex(value: String, message: String) -> Self {
119-
return Self(isExternal: false) {
120-
NSPredicate(format: "SELF MATCHES %@", value)
121-
.evaluate(with: $0) ? nil : message
131+
public static func regex(conditions: [ValidationBehaviour], value: String, message: String) -> Self {
132+
return Self(conditions: conditions, isExternal: false) {
133+
return (NSPredicate(format: "SELF MATCHES %@", value).evaluate(with: $0), message)
122134
}
123135
}
124136
}

Sources/FormView/Validation/Validators/FieldValidator.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@ struct FieldValidator {
1414
self.rules = rules
1515
}
1616

17-
func validate(value: String, isNeedToCheckExternal: Bool) async -> [ValidationRule] {
17+
func validate(
18+
value: String,
19+
condition: ValidationBehaviour,
20+
isNeedToCheckExternal: Bool
21+
) async -> [ValidationRule] {
1822
var failedRules: [ValidationRule] = []
1923

20-
for rule in rules where rule.isExternal == false || isNeedToCheckExternal {
24+
for rule in rules where (rule.isExternal == false || isNeedToCheckExternal)
25+
&& rule.conditions.contains(condition) {
2126
if await rule.check(value: value) == false {
2227
failedRules.append(rule)
2328
}

0 commit comments

Comments
 (0)