Skip to content

Commit 41a2f81

Browse files
authored
Merge pull request #10 from GoodRequest/feature/main-validation
Isolate validation to main thread + fix error localizations
2 parents 994fd02 + 1664b16 commit 41a2f81

File tree

8 files changed

+64
-79
lines changed

8 files changed

+64
-79
lines changed

GoodSwiftUI-Sample/GoodSwiftUI-Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ struct InputFieldSampleView: View {
1616
case notFilip
1717
case pinTooShort
1818

19-
var localizedDescription: String {
19+
var errorDescription: String? {
2020
switch self {
2121
case .notFilip:
2222
"Your name is not Filip"
@@ -154,6 +154,15 @@ extension InputFieldSampleView {
154154
placeholder: "0 %"
155155
)
156156
.inputFieldTraits(keyboardType: .numbersAndPunctuation)
157+
.onSubmit {
158+
print("Submit action")
159+
}
160+
.onResign {
161+
print("Resign action")
162+
}
163+
.onEditingChanged {
164+
print("Value changed")
165+
}
157166
}
158167

159168
private var customViewsInputField: some View {

Package.resolved

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ let package = Package(
2323
)
2424
],
2525
dependencies: [
26-
.package(url: "https://github.com/goodrequest/goodextensions-ios", .upToNextMajor(from: "2.0.0"))
26+
.package(url: "https://github.com/GoodRequest/GoodExtensions-iOS.git", .upToNextMajor(from: "2.0.2"))
2727
],
2828
targets: [
2929
.target(

Sources/GRInputField/Common/ValidationError.swift

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,16 @@ import GoodExtensions
1010

1111
// MARK: - Validation errors
1212

13-
public protocol ValidationError: Error, Equatable {
14-
15-
var localizedDescription: String { get }
16-
17-
}
13+
public protocol ValidationError: LocalizedError {}
1814

1915
public enum InternalValidationError: ValidationError {
2016

2117
case alwaysError
2218
case required
2319
case mismatch
24-
case external(String)
20+
case external(MainSupplier<String>)
2521

26-
public var localizedDescription: String {
22+
public var errorDescription: String? {
2723
switch self {
2824
case .alwaysError:
2925
"Error"
@@ -35,7 +31,9 @@ public enum InternalValidationError: ValidationError {
3531
"Elements do not match"
3632

3733
case .external(let description):
38-
description
34+
MainActor.assumeIsolated {
35+
description()
36+
}
3937
}
4038
}
4139

@@ -46,18 +44,18 @@ public enum InternalValidationError: ValidationError {
4644
public extension Criterion {
4745

4846
/// Always succeeds
49-
nonisolated static let alwaysValid = Criterion { _ in true }
47+
static let alwaysValid = Criterion { _ in true }
5048

5149
/// Always fails
52-
nonisolated static let alwaysError = Criterion { _ in false }
50+
static let alwaysError = Criterion { _ in false }
5351
.failWith(error: InternalValidationError.alwaysError)
5452

5553
/// Accepts any input with length > 0, excluding leading/trailing whitespace
56-
nonisolated static let nonEmpty = Criterion { !($0 ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
54+
static let nonEmpty = Criterion { !($0 ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
5755
.failWith(error: InternalValidationError.required)
5856

5957
/// Accepts an input if it is equal with another input
60-
nonisolated static func matches(_ other: String?) -> Criterion {
58+
static func matches(_ other: String?) -> Criterion {
6159
Criterion { this in this == other }
6260
.failWith(error: InternalValidationError.mismatch)
6361
}
@@ -69,14 +67,14 @@ public extension Criterion {
6967
///
7068
/// If input is empty, validation **succeeds** and input is deemed valid.
7169
/// If input is non-empty, validation continues by criterion specified as a parameter.
72-
nonisolated static func acceptEmpty(_ criterion: Criterion) -> Criterion {
70+
static func acceptEmpty(_ criterion: Criterion) -> Criterion {
7371
Criterion { Criterion.nonEmpty.validate(input: $0) ? criterion.validate(input: $0) : true }
7472
.failWith(error: criterion.error)
7573
}
7674

77-
nonisolated static func external(error: @autoclosure @escaping Supplier<(any Error)?>) -> Criterion {
75+
static func external(error: @MainActor @escaping () -> (any LocalizedError)?) -> Criterion {
7876
Criterion { _ in error().isNil }
79-
.failWith(error: InternalValidationError.external(error()?.localizedDescription ?? " "))
77+
.failWith(error: InternalValidationError.external { error()?.localizedDescription ?? " " })
8078
}
8179

8280
}

Sources/GRInputField/Common/Validator.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import GoodStructs
1111

1212
// MARK: - ValidatorBuilder
1313

14-
@resultBuilder public struct ValidatorBuilder {
14+
@MainActor @resultBuilder public struct ValidatorBuilder {
1515

1616
public static func buildBlock(_ components: CriteriaConvertible...) -> Validator {
1717
var criteria: [Criterion] = []
@@ -48,15 +48,15 @@ import GoodStructs
4848

4949
// MARK: - Validator
5050

51-
public struct Validator: CriteriaConvertible {
51+
@MainActor public struct Validator: CriteriaConvertible {
5252

53-
fileprivate var criteria: [Criterion] = []
53+
internal var criteria: [Criterion] = []
5454

55-
@MainActor public func isValid(input: String?) -> Bool {
55+
public func isValid(input: String?) -> Bool {
5656
validate(input: input).isNil
5757
}
5858

59-
@MainActor public func validate(input: String?) -> (any ValidationError)? {
59+
public func validate(input: String?) -> (any ValidationError)? {
6060
let failedCriterion = criteria
6161
.map { (criterion: $0, result: $0.validate(input: input)) }
6262
.first { _, result in !result }
@@ -77,7 +77,7 @@ public struct Validator: CriteriaConvertible {
7777

7878
// MARK: - Criterion
7979

80-
public struct Criterion: Sendable, Then, CriteriaConvertible {
80+
@MainActor public struct Criterion: Sendable, Then, CriteriaConvertible {
8181

8282
// MARK: - Variables
8383

@@ -114,7 +114,7 @@ public struct Criterion: Sendable, Then, CriteriaConvertible {
114114

115115
// MARK: - CriteriaConvertible
116116

117-
public protocol CriteriaConvertible {
117+
@MainActor public protocol CriteriaConvertible {
118118

119119
func asCriteria() -> [Criterion]
120120

@@ -124,11 +124,11 @@ public protocol CriteriaConvertible {
124124

125125
extension Criterion: Hashable {
126126

127-
public static func == (lhs: Criterion, rhs: Criterion) -> Bool {
127+
nonisolated public static func == (lhs: Criterion, rhs: Criterion) -> Bool {
128128
lhs.hashValue == rhs.hashValue
129129
}
130130

131-
public func hash(into hasher: inout Hasher) {
131+
nonisolated public func hash(into hasher: inout Hasher) {
132132
if predicate.isNotNil { hasher.combine(UUID()) }
133133
hasher.combine(regex)
134134
}

Sources/GRInputField/SwiftUI/InputField.swift

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public struct InputField<LeftView: View, RightView: View>: UIViewRepresentable {
3636

3737
private var traits: InputFieldTraits
3838
private var allowedInput: Regex?
39-
@ValidatorBuilder private var criteria: Supplier<Validator>
39+
@ValidatorBuilder private var criteria: MainSupplier<Validator>
4040
private var focusAction: MainClosure?
4141
private var submitAction: MainClosure?
4242
private var resignAction: MainClosure?
@@ -58,14 +58,7 @@ public struct InputField<LeftView: View, RightView: View>: UIViewRepresentable {
5858
self.placeholder = placeholder
5959
self.hint = hint
6060
self.traits = InputFieldTraits()
61-
62-
@ValidatorBuilder @Sendable func alwaysValidCriteria() -> Validator {
63-
Criterion.alwaysValid
64-
}
65-
66-
// closures cannot take @ValidationBuilder attribute, must be a function reference
67-
self.criteria = alwaysValidCriteria
68-
61+
self.criteria = { Validator(criteria: [Criterion.alwaysValid]) }
6962
self.leftView = leftView
7063
self.rightView = rightView
7164
}
@@ -373,25 +366,25 @@ public extension InputField {
373366
return modifiedSelf
374367
}
375368

376-
func onResign(_ action: @escaping VoidClosure) -> Self {
369+
func onResign(_ action: @escaping MainClosure) -> Self {
377370
var modifiedSelf = self
378371
modifiedSelf.resignAction = action
379372
return modifiedSelf
380373
}
381374

382-
func onSubmit(_ action: @escaping VoidClosure) -> Self {
375+
func onSubmit(_ action: @escaping MainClosure) -> Self {
383376
var modifiedSelf = self
384377
modifiedSelf.submitAction = action
385378
return modifiedSelf
386379
}
387380

388-
func onEditingChanged(_ action: @escaping VoidClosure) -> Self {
381+
func onEditingChanged(_ action: @escaping MainClosure) -> Self {
389382
var modifiedSelf = self
390383
modifiedSelf.editingChangedAction = action
391384
return modifiedSelf
392385
}
393386

394-
func validationCriteria(@ValidatorBuilder _ criteria: @escaping Supplier<Validator>) -> Self {
387+
func validationCriteria(@ValidatorBuilder _ criteria: @escaping MainSupplier<Validator>) -> Self {
395388
var modifiedSelf = self
396389
modifiedSelf.criteria = criteria
397390
return modifiedSelf
@@ -426,7 +419,6 @@ public extension InputField {
426419
func allowedInput(_ allowedInputRegex: Regex?) -> Self {
427420
var modifiedSelf = self
428421
modifiedSelf.allowedInput = allowedInputRegex
429-
430422
return modifiedSelf
431423
}
432424

Sources/GRInputField/SwiftUI/ValidityGroup.swift

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111

1212
public typealias ValidityGroup = [UUID: ValidationState]
1313

14-
public extension ValidityGroup {
14+
@MainActor public extension ValidityGroup {
1515

1616
func allValid() -> Bool {
1717
isEmpty ? false : allSatisfy { $0.value.isValid }
@@ -25,7 +25,7 @@ public extension ValidityGroup {
2525

2626
// MARK: - Validation State
2727

28-
public enum ValidationState: Equatable, Sendable {
28+
@MainActor public enum ValidationState: Sendable, Equatable {
2929

3030
case valid
3131
case error(any ValidationError)
@@ -56,36 +56,22 @@ public enum ValidationState: Equatable, Sendable {
5656
}
5757
}
5858

59-
public static func == (lhs: ValidationState, rhs: ValidationState) -> Bool {
60-
switch lhs {
61-
case .valid:
62-
switch rhs {
63-
case .valid:
64-
return true
65-
default:
66-
return false
67-
}
68-
case .error(let lhsValidationError):
69-
switch rhs {
70-
case .error(let rhsValidationError):
71-
return lhsValidationError.localizedDescription == rhsValidationError.localizedDescription
72-
default:
73-
return false
74-
}
75-
case .pending(let lhsValidationError):
76-
switch rhs {
77-
case .pending(let rhsValidationError):
78-
return lhsValidationError?.localizedDescription == rhsValidationError?.localizedDescription
79-
default:
80-
return false
81-
}
82-
case .invalid:
83-
switch rhs {
84-
case .invalid:
85-
return true
86-
default:
87-
return false
88-
}
59+
nonisolated public static func == (lhs: ValidationState, rhs: ValidationState) -> Bool {
60+
switch (lhs, rhs) {
61+
case (.valid, .valid):
62+
return true
63+
64+
case (.invalid, .invalid):
65+
return true
66+
67+
case (.error(let lhsValidationError), .error(let rhsValidationError)):
68+
return lhsValidationError.localizedDescription == rhsValidationError.localizedDescription
69+
70+
case (.pending(let lhsValidationError), .pending(let rhsValidationError)):
71+
return lhsValidationError?.localizedDescription == rhsValidationError?.localizedDescription
72+
73+
default:
74+
return false
8975
}
9076
}
9177

0 commit comments

Comments
 (0)