Skip to content

Commit 9b4d743

Browse files
committed
Implement a form validation logic
1 parent 43fbc45 commit 9b4d743

8 files changed

Lines changed: 290 additions & 0 deletions

File tree

Sources/ValidatorUI/Classes/SUI/Extensions/View+ValidationModifier.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import SwiftUI
77
import ValidatorCore
88

99
public extension View {
10+
/// Creates a view validation modifier.
11+
///
12+
/// - Parameters:
13+
/// - item: The binding item to validate.
14+
/// - rules: The array of validation rules to apply to the item’s value.
15+
/// - content: A custom parameter attribute that constructs an error view from closures.
16+
///
17+
/// - Returns: A modified view.
1018
func validate<T, ErrorView: View>(
1119
item: Binding<T>,
1220
rules: [any IValidationRule<T>],
@@ -20,4 +28,23 @@ public extension View {
2028
)
2129
)
2230
}
31+
32+
/// Creates a view validation modifier.
33+
///
34+
/// - Parameters:
35+
/// - validationContainer: The container to validate.
36+
/// - content: A custom parameter attribute that constructs an error view from closures.
37+
///
38+
/// - Returns: A modified view.
39+
func validate<ErrorView: View>(
40+
validationContainer: any IFormValidationContainer,
41+
@ViewBuilder content: @escaping ([any IValidationError]) -> ErrorView
42+
) -> some View {
43+
modifier(
44+
FormValidationViewModifier(
45+
validationContainer: validationContainer,
46+
content: content
47+
)
48+
)
49+
}
2350
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import Combine
7+
import Foundation
8+
import ValidatorCore
9+
10+
public typealias ValidationPublisher = AnyPublisher<ValidationResult, Never>
11+
12+
// MARK: - FormField
13+
14+
@propertyWrapper
15+
public final class FormField<Value>: IFormField {
16+
// MARK: Properties
17+
18+
@Published
19+
/// The value to validate.
20+
private var value: Value
21+
22+
/// The validation.
23+
private let validator: IValidator
24+
25+
/// The validation rules.
26+
private let rules: [any IValidationRule<Value>]
27+
28+
public var wrappedValue: Value {
29+
get { value }
30+
set { value = newValue }
31+
}
32+
33+
// MARK: Initialization
34+
35+
public init(
36+
wrappedValue: Value,
37+
validator: IValidator = Validator(),
38+
rules: [any IValidationRule<Value>]
39+
) {
40+
value = wrappedValue
41+
self.validator = validator
42+
self.rules = rules
43+
}
44+
45+
// MARK: IFormField
46+
47+
public func validate(manager: some IFormFieldManager) -> any IFormValidationContainer {
48+
let subject = CurrentValueSubject<Value, Never>(value)
49+
50+
let publisher = $value
51+
.receive(on: RunLoop.main)
52+
.handleEvents(receiveOutput: { subject.send($0) })
53+
.map { self.validator.validate(input: $0, rules: self.rules) }
54+
.eraseToAnyPublisher()
55+
56+
let container = FormValidationContainter(
57+
value: subject,
58+
publisher: publisher,
59+
validator: validator,
60+
rules: rules
61+
)
62+
63+
manager.append(validator: container)
64+
65+
return container
66+
}
67+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import Combine
7+
import Foundation
8+
import ValidatorCore
9+
10+
/// A type that represents a field on a form.
11+
public protocol IFormField {
12+
/// Performs field validation.
13+
///
14+
/// - Note: Create a form validation container that keeps track of the validation.
15+
///
16+
/// - Parameter manager: The form field manager.
17+
///
18+
/// - Returns: A validation container.
19+
func validate(manager: some IFormFieldManager) -> any IFormValidationContainer
20+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import Combine
7+
import Foundation
8+
import ValidatorCore
9+
10+
public final class FormFieldManager: IFormFieldManager {
11+
// MARK: Properties
12+
13+
@Published public var isValid = true
14+
15+
private var validators: [any IFormValidationContainer] = []
16+
17+
// MARK: Initialization
18+
19+
public init() {}
20+
21+
// MARK: IFormFieldManager
22+
23+
public func append(validator: some IFormValidationContainer) {
24+
validators.append(validator)
25+
validate()
26+
}
27+
28+
public func validate() {
29+
// swiftlint:disable:next contains_over_filter_is_empty
30+
isValid = validators
31+
.filter { $0.validate() != .valid }
32+
.isEmpty
33+
}
34+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import Combine
7+
import Foundation
8+
import ValidatorCore
9+
10+
/// A type that manages the validation state of a form.
11+
public protocol IFormFieldManager: ObservableObject {
12+
/// A Boolean value that indicates whether all fields on a form are valid or not.
13+
var isValid: Bool { get }
14+
15+
/// Appends a new validator to the manager.
16+
///
17+
/// - Parameter validator: The validation container that encompasses required validation logic.
18+
func append(validator: some IFormValidationContainer)
19+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import Combine
7+
import Foundation
8+
import ValidatorCore
9+
10+
public struct FormValidationContainter<T>: IFormValidationContainer {
11+
// MARK: Properties
12+
13+
public var value: FormValidatorValueSubject<T>
14+
public let publisher: ValidationPublisher
15+
public let validator: IValidator
16+
public let rules: [any IValidationRule<T>]
17+
18+
// MARK: Initialization
19+
20+
public init(
21+
value: FormValidatorValueSubject<T>,
22+
publisher: ValidationPublisher,
23+
validator: IValidator,
24+
rules: [any IValidationRule<T>]
25+
) {
26+
self.value = value
27+
self.publisher = publisher
28+
self.validator = validator
29+
self.rules = rules
30+
}
31+
32+
// MARK: IFormValidationContainer
33+
34+
public func validate() -> ValidationResult {
35+
validator.validate(input: value.value, rules: rules)
36+
}
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import Combine
7+
import Foundation
8+
import ValidatorCore
9+
10+
public typealias FormValidatorValueSubject<Value> = CurrentValueSubject<Value, Never>
11+
12+
// MARK: - IFormValidationContainer
13+
14+
/// A container for form validation logic.
15+
public protocol IFormValidationContainer<Value> {
16+
associatedtype Value
17+
18+
/// The value subject used for form validation.
19+
var value: FormValidatorValueSubject<Value> { get }
20+
21+
/// The publisher responsible for emitting validation events.
22+
var publisher: ValidationPublisher { get }
23+
24+
/// The validator associated with this validation container.
25+
var validator: IValidator { get }
26+
27+
/// An array of validation rules to apply to the form field.
28+
var rules: [any IValidationRule<Value>] { get }
29+
30+
/// Performs form field validation.
31+
///
32+
/// - Returns: The result of the validation process.
33+
func validate() -> ValidationResult
34+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Validator
3+
// Copyright © 2023 Space Code. All rights reserved.
4+
//
5+
6+
import SwiftUI
7+
import ValidatorCore
8+
9+
public struct FormValidationViewModifier<ErrorView: View>: ViewModifier {
10+
// MARK: Properties
11+
12+
/// The result of the validation.
13+
@State private var validationResult: ValidationResult = .valid
14+
15+
/// A container for form validation logic.
16+
private let validationContainer: any IFormValidationContainer
17+
18+
/// A custom parameter attribute that constructs views from closures.
19+
@ViewBuilder private let content: ([any IValidationError]) -> ErrorView
20+
21+
// MARK: Initialization
22+
23+
public init(
24+
validationContainer: any IFormValidationContainer,
25+
@ViewBuilder content: @escaping ([any IValidationError]) -> ErrorView
26+
) {
27+
self.validationContainer = validationContainer
28+
self.content = content
29+
}
30+
31+
// MARK: ViewModifier
32+
33+
public func body(content: Content) -> some View {
34+
VStack(alignment: .leading) {
35+
content
36+
validationMessageView
37+
}.onReceive(validationContainer.publisher) { result in
38+
self.validationResult = result
39+
}
40+
}
41+
42+
// MARK: Private
43+
44+
private var validationMessageView: some View {
45+
switch validationResult {
46+
case .valid:
47+
return EmptyView().eraseToAnyView()
48+
case let .invalid(errors):
49+
return content(errors).eraseToAnyView()
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)