Skip to content

Commit 8e7fd10

Browse files
committed
Isolate validation to @mainactor
1 parent 994fd02 commit 8e7fd10

File tree

5 files changed

+36
-72
lines changed

5 files changed

+36
-72
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {

Sources/GRInputField/Common/ValidationError.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import GoodExtensions
1010

1111
// MARK: - Validation errors
1212

13-
public protocol ValidationError: Error, Equatable {
13+
@MainActor public protocol ValidationError: Error {
1414

1515
var localizedDescription: String { get }
1616

@@ -21,7 +21,7 @@ public enum InternalValidationError: ValidationError {
2121
case alwaysError
2222
case required
2323
case mismatch
24-
case external(String)
24+
case external(MainSupplier<String>)
2525

2626
public var localizedDescription: String {
2727
switch self {
@@ -35,7 +35,7 @@ public enum InternalValidationError: ValidationError {
3535
"Elements do not match"
3636

3737
case .external(let description):
38-
description
38+
description()
3939
}
4040
}
4141

@@ -46,18 +46,18 @@ public enum InternalValidationError: ValidationError {
4646
public extension Criterion {
4747

4848
/// Always succeeds
49-
nonisolated static let alwaysValid = Criterion { _ in true }
49+
static let alwaysValid = Criterion { _ in true }
5050

5151
/// Always fails
52-
nonisolated static let alwaysError = Criterion { _ in false }
52+
static let alwaysError = Criterion { _ in false }
5353
.failWith(error: InternalValidationError.alwaysError)
5454

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

5959
/// Accepts an input if it is equal with another input
60-
nonisolated static func matches(_ other: String?) -> Criterion {
60+
static func matches(_ other: String?) -> Criterion {
6161
Criterion { this in this == other }
6262
.failWith(error: InternalValidationError.mismatch)
6363
}
@@ -69,14 +69,14 @@ public extension Criterion {
6969
///
7070
/// If input is empty, validation **succeeds** and input is deemed valid.
7171
/// If input is non-empty, validation continues by criterion specified as a parameter.
72-
nonisolated static func acceptEmpty(_ criterion: Criterion) -> Criterion {
72+
static func acceptEmpty(_ criterion: Criterion) -> Criterion {
7373
Criterion { Criterion.nonEmpty.validate(input: $0) ? criterion.validate(input: $0) : true }
7474
.failWith(error: criterion.error)
7575
}
7676

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

8282
}

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 & 18 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
}
@@ -325,10 +318,6 @@ public struct InputField<LeftView: View, RightView: View>: UIViewRepresentable {
325318
return syncValidationState(uiViewState: .pending(error: validationError), context: context)
326319
}
327320

328-
guard newValidationState != previousValidationState else {
329-
return
330-
}
331-
332321
changeValidationState(to: newValidationState, uiView: uiView, context: context)
333322
}
334323

@@ -373,25 +362,25 @@ public extension InputField {
373362
return modifiedSelf
374363
}
375364

376-
func onResign(_ action: @escaping VoidClosure) -> Self {
365+
func onResign(_ action: @escaping MainClosure) -> Self {
377366
var modifiedSelf = self
378367
modifiedSelf.resignAction = action
379368
return modifiedSelf
380369
}
381370

382-
func onSubmit(_ action: @escaping VoidClosure) -> Self {
371+
func onSubmit(_ action: @escaping MainClosure) -> Self {
383372
var modifiedSelf = self
384373
modifiedSelf.submitAction = action
385374
return modifiedSelf
386375
}
387376

388-
func onEditingChanged(_ action: @escaping VoidClosure) -> Self {
377+
func onEditingChanged(_ action: @escaping MainClosure) -> Self {
389378
var modifiedSelf = self
390379
modifiedSelf.editingChangedAction = action
391380
return modifiedSelf
392381
}
393382

394-
func validationCriteria(@ValidatorBuilder _ criteria: @escaping Supplier<Validator>) -> Self {
383+
func validationCriteria(@ValidatorBuilder _ criteria: @escaping MainSupplier<Validator>) -> Self {
395384
var modifiedSelf = self
396385
modifiedSelf.criteria = criteria
397386
return modifiedSelf
@@ -426,7 +415,6 @@ public extension InputField {
426415
func allowedInput(_ allowedInputRegex: Regex?) -> Self {
427416
var modifiedSelf = self
428417
modifiedSelf.allowedInput = allowedInputRegex
429-
430418
return modifiedSelf
431419
}
432420

Sources/GRInputField/SwiftUI/ValidityGroup.swift

Lines changed: 2 additions & 35 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 {
2929

3030
case valid
3131
case error(any ValidationError)
@@ -56,37 +56,4 @@ 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-
}
89-
}
90-
}
91-
9259
}

0 commit comments

Comments
 (0)