Skip to content

Commit 7a78b9b

Browse files
committed
Realtime criteria
1 parent 7482156 commit 7a78b9b

File tree

4 files changed

+67
-7
lines changed

4 files changed

+67
-7
lines changed

GoodSwiftUI-Sample/GoodSwiftUI-Sample/Screens/InputFieldSampleView.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct InputFieldSampleView: View {
1515

1616
case notFilip
1717
case pinTooShort
18+
case pinTooLong
1819

1920
var errorDescription: String? {
2021
switch self {
@@ -23,6 +24,9 @@ struct InputFieldSampleView: View {
2324

2425
case .pinTooShort:
2526
"PIN code must be at least 6 numbers long"
27+
28+
case .pinTooLong:
29+
"PIN code is too long"
2630
}
2731
}
2832

@@ -119,6 +123,9 @@ extension InputFieldSampleView {
119123
// Focus state binding to advance focus from keyboard action button (Continue)
120124
.bindFocusState($focusState, to: .name)
121125
.disabled(!nameEnabled)
126+
127+
// Accessibility identifier for UI tests
128+
.accessibilityIdentifier("nameTextField")
122129
}
123130

124131
private var pinCodeInputField: some View {
@@ -138,11 +145,21 @@ extension InputFieldSampleView {
138145
password?.count ?? 0 >= 6
139146
}
140147
.failWith(error: RegistrationError.pinTooShort)
148+
.realtime()
149+
150+
Criterion { password in
151+
password?.count ?? 1000 <= 8
152+
}
153+
.failWith(error: RegistrationError.pinTooLong)
154+
.realtime()
141155
}
142156

143157
.validityGroup($validityGroup)
144158
.bindFocusState($focusState, to: .pin)
145159
.disabled(!passwordEnabled)
160+
161+
// Accessibility identifier for UI tests
162+
.accessibilityIdentifier("pinTextField")
146163
}
147164

148165
private var percentFormattedInputField: some View {
@@ -185,7 +202,8 @@ extension InputFieldSampleView {
185202
}
186203
}
187204
)
188-
.alert("Alert", isPresented: $showsRightAlert, actions: {})
205+
.alert("Right button alert", isPresented: $showsRightAlert, actions: {})
206+
.accessibilityIdentifier("customViewsInputField")
189207
}
190208

191209
private var validityGroups: some View {

Sources/GRInputField/Common/Validator.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ import GoodStructs
5252

5353
internal var criteria: [Criterion] = []
5454

55+
internal init(criteria: [Criterion]) {
56+
self.criteria = criteria
57+
}
58+
59+
internal init(realtime criteria: [Criterion]) {
60+
self.criteria = criteria.filter { $0.isRealtime }
61+
}
62+
5563
public func isValid(input: String?) -> Bool {
5664
validate(input: input).isNil
5765
}
@@ -82,6 +90,7 @@ import GoodStructs
8290
// MARK: - Variables
8391

8492
private(set) internal var error: any ValidationError = InternalValidationError.invalid
93+
private(set) internal var isRealtime: Bool = false
8594

8695
// MARK: - Constants
8796

@@ -106,6 +115,12 @@ import GoodStructs
106115
return mutableSelf
107116
}
108117

118+
public func realtime() -> Self {
119+
var mutableSelf = self
120+
mutableSelf.isRealtime = true
121+
return mutableSelf
122+
}
123+
109124
public func asCriteria() -> [Criterion] {
110125
[self]
111126
}

Sources/GRInputField/SwiftUI/InputField.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ public struct InputField<LeftView: View, RightView: View>: UIViewRepresentable {
268268
// MARK: - Validation
269269

270270
private func syncValidationState(uiViewState validationState: ValidationState, context: InputField.Context) {
271-
Task { @MainActor in
271+
DispatchQueue.main.async {
272272
validityGroup.updateValue(
273273
validationState,
274274
forKey: context.coordinator.textFieldUUID
@@ -294,7 +294,11 @@ public struct InputField<LeftView: View, RightView: View>: UIViewRepresentable {
294294

295295
private func updateValidationState(uiView: ValidableInputFieldView, context: Context) {
296296
let previousValidationState = validityGroup[context.coordinator.textFieldUUID]
297-
let validationError = criteria().validate(input: text)
297+
298+
let validator = criteria()
299+
let validationError = validator.validate(input: text)
300+
let realtimeError = Validator(realtime: validator.asCriteria()).validate(input: text)
301+
298302
let newValidationState: ValidationState = if let validationError {
299303
.error(validationError)
300304
} else {
@@ -310,6 +314,14 @@ public struct InputField<LeftView: View, RightView: View>: UIViewRepresentable {
310314
}
311315
}
312316

317+
if let realtimeError, !uiView.text.isEmpty {
318+
changeValidationState(
319+
to: .error(realtimeError),
320+
uiView: uiView,
321+
context: context
322+
)
323+
}
324+
313325
if case .pending = previousValidationState {
314326
return
315327
}

Sources/GRInputField/UIKit/ValidableInputFieldView.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@ public class ValidableInputFieldView: InputFieldView {
4545
/// Requests validation of this text fields' content. Text field will automatically update the appearance to
4646
/// valid/invalid state, depending on the result of validation.
4747
///
48-
/// If no validator is set, this function will crash. Please make sure to set validation criteria before
49-
/// requesting validation.
48+
/// Please make sure to set validation criteria before requesting validation. Otherwise the result will
49+
/// be always `false` and the function will fail assertion in debug builds.
5050
/// - Returns: `true` when content is valid, `false` otherwise
5151
@discardableResult
5252
public func validate() -> Bool {
53-
guard let validator = validator?() else { fatalError("Validator not set") }
53+
guard let validator = validator?() else {
54+
assertionFailure("Validator not set")
55+
return false
56+
}
5457

5558
if let error = validator.validate(input: self.text) {
5659
fail(with: error.localizedDescription)
@@ -80,7 +83,19 @@ public class ValidableInputFieldView: InputFieldView {
8083
.map { [weak self] _ in self?.validator?().validate(input: self?.text) }
8184
.sink { [weak self] error in
8285
if let error { self?.fail(with: error.localizedDescription) }
83-
self?.afterValidation?(error) // If wrapped in UIViewRepresentable, update SwiftUI state here
86+
87+
// If wrapped in UIViewRepresentable, update SwiftUI state here
88+
self?.afterValidation?(error)
89+
}
90+
.store(in: &cancellables)
91+
92+
editingChangedPublisher
93+
.map { [weak self] _ in
94+
Validator(realtime: self?.validator?().asCriteria() ?? [])
95+
.validate(input: self?.text)
96+
}
97+
.sink { [weak self] error in
98+
if let error { self?.failSilently(with: error.localizedDescription) }
8499
}
85100
.store(in: &cancellables)
86101
}

0 commit comments

Comments
 (0)