From f6c25805f1d2dc8c1d16754c5285f3456eb5ebe5 Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Mon, 10 Mar 2025 20:15:33 +0300 Subject: [PATCH 01/13] fix tests and update ViewInspector to 0.10.1 --- Package.resolved | 4 +- Package.swift | 2 +- Tests/FormViewTests/FormViewTests.swift | 56 ++++++++++++++----- .../Validation/TextValidationRuleTests.swift | 30 +++++----- .../Validation/ValidatorTests.swift | 23 ++++---- 5 files changed, 69 insertions(+), 46 deletions(-) diff --git a/Package.resolved b/Package.resolved index da2cfba..5868e83 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nalexn/ViewInspector", "state" : { - "branch" : "master", - "revision" : "4effbd9143ab797eb60d2f32d4265c844c980946" + "revision" : "788e7879d38a839c4e348ab0762dcc0364e646a2", + "version" : "0.10.1" } } ], diff --git a/Package.swift b/Package.swift index f92de2c..d4ae6c4 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( targets: ["FormView"]) ], dependencies: [ - .package(url: "https://github.com/nalexn/ViewInspector", branch: "master") + .package(url: "https://github.com/nalexn/ViewInspector", exact: .init(0, 10, 1)) ], targets: [ .target( diff --git a/Tests/FormViewTests/FormViewTests.swift b/Tests/FormViewTests/FormViewTests.swift index 726a10b..f95e4fc 100644 --- a/Tests/FormViewTests/FormViewTests.swift +++ b/Tests/FormViewTests/FormViewTests.swift @@ -12,18 +12,28 @@ import Combine @testable import FormView final class FormViewTests: XCTestCase { + @MainActor func testPreventInvalidInput() throws { var text1 = "" var text2 = "" let sut = InspectionWrapperView( - wrapped: FormView { + wrapped: FormView { _ in ScrollView { FormField( value: Binding(get: { text1 }, set: { text1 = $0 }), - validationRules: [.digitsOnly] + rules: [TextValidationRule.digitsOnly(message: "")], + content: { _ in + TextField(text1, text: Binding(get: { text1 }, set: { text1 = $0 })) + } ) .id(1) - FormField(value: Binding(get: { text2 }, set: { text2 = $0 })) + FormField( + value: Binding(get: { text2 }, set: { text2 = $0 }), + rules: [TextValidationRule.digitsOnly(message: "")], + content: { _ in + TextField(text2, text: Binding(get: { text2 }, set: { text2 = $0 })) + } + ) .id(2) } } @@ -31,34 +41,45 @@ final class FormViewTests: XCTestCase { let exp = sut.inspection.inspect { view in let scrollView = try view.find(ViewType.ScrollView.self) - let textField1 = try view.find(viewWithId: 1).textField() + let textField1 = try view.find(viewWithId: 1).find(ViewType.TextField.self) + + text1 = "123" try scrollView.callOnSubmit() try textField1.callOnChange(newValue: "New Focus Field", index: 1) try textField1.callOnChange(newValue: "123") XCTAssertEqual(try textField1.input(), "123") - text1 = "123" try textField1.callOnChange(newValue: "123_A") - XCTAssertEqual(try textField1.input(), text1) + XCTAssertNotEqual(try textField1.input(), "123_A") } ViewHosting.host(view: sut) wait(for: [exp], timeout: 0.1) } + @MainActor func testSubmitTextField() throws { var text1 = "" var text2 = "" let sut = InspectionWrapperView( - wrapped: FormView { + wrapped: FormView { _ in ScrollView { FormField( value: Binding(get: { text1 }, set: { text1 = $0 }), - validationRules: [.digitsOnly] + rules: [TextValidationRule.digitsOnly(message: "")], + content: { _ in + TextField(text1, text: Binding(get: { text1 }, set: { text1 = $0 })) + } ) .id(1) - FormField(value: Binding(get: { text2 }, set: { text2 = $0 })) + FormField( + value: Binding(get: { text2 }, set: { text2 = $0 }), + rules: [TextValidationRule.digitsOnly(message: "")], + content: { _ in + TextField(text2, text: Binding(get: { text2 }, set: { text2 = $0 })) + } + ) .id(2) } } @@ -66,7 +87,7 @@ final class FormViewTests: XCTestCase { let exp = sut.inspection.inspect { view in let scrollView = try view.find(ViewType.ScrollView.self) - let textField1 = try view.find(viewWithId: 1).textField() + let textField1 = try view.find(viewWithId: 1).find(ViewType.TextField.self) // let formField2 = try view.find(viewWithId: 2).view(FormField.self).actualView() try scrollView.callOnSubmit() @@ -76,17 +97,22 @@ final class FormViewTests: XCTestCase { XCTAssertTrue(true) } - ViewHosting.host(view: sut.environment(\.focusField, "field1")) + ViewHosting.host(view: sut.environment(\.focusedFieldId, "1")) wait(for: [exp], timeout: 0.1) } func testFocusNextField() throws { - var fieldStates = [FieldState(id: "1", isFocused: false), FieldState(id: "2", isFocused: false)] + let fieldStates = [ + FieldState(id: "1", isFocused: true, onValidate: { true }), + FieldState(id: "2", isFocused: false, onValidate: { false }) + ] - var nextFocusField = fieldStates.focusNextField(currentFocusField: "") - XCTAssertEqual(nextFocusField, "1") + var nextFocusField = FocusService.getNextFocusFieldId(states: fieldStates, currentFocusField: "1") + nextFocusField = nextFocusField.trimmingCharacters(in: .whitespaces) + XCTAssertEqual(nextFocusField, "2") - nextFocusField = fieldStates.focusNextField(currentFocusField: "1") + nextFocusField = FocusService.getNextFocusFieldId(states: fieldStates, currentFocusField: "2") + nextFocusField = nextFocusField.trimmingCharacters(in: .whitespaces) XCTAssertEqual(nextFocusField, "2") } } diff --git a/Tests/FormViewTests/Validation/TextValidationRuleTests.swift b/Tests/FormViewTests/Validation/TextValidationRuleTests.swift index b6dc3f5..a4e2569 100644 --- a/Tests/FormViewTests/Validation/TextValidationRuleTests.swift +++ b/Tests/FormViewTests/Validation/TextValidationRuleTests.swift @@ -10,64 +10,64 @@ import XCTest final class TextValidationRuleTests: XCTestCase { func testIgnoreEmpty() throws { - try test(textRule: .digitsOnly, trueString: "", falseString: "1234 A") + try test(textRule: .digitsOnly(message: ""), trueString: "", falseString: "1234 A") } func testNotEmpty() throws { - try test(textRule: .notEmpty, trueString: "Not empty", falseString: "") + try test(textRule: .notEmpty(message: ""), trueString: "Not empty", falseString: "") } func testMinLength() throws { - try test(textRule: .minLength(4), trueString: "1234", falseString: "123") + try test(textRule: .minLength(count: 4, message: ""), trueString: "1234", falseString: "123") } func testMaxLength() throws { - try test(textRule: .maxLength(4), trueString: "1234", falseString: "123456") + try test(textRule: .maxLength(count: 4, message: ""), trueString: "1234", falseString: "123456") } func testAtLeastOneDigit() throws { - try test(textRule: .atLeastOneDigit, trueString: "Digit 5", falseString: "No Digits") + try test(textRule: .atLeastOneDigit(message: ""), trueString: "Digit 5", falseString: "No Digits") } func testAtLeastOneLetter() throws { - try test(textRule: .atLeastOneLetter, trueString: "1234 A", falseString: "1234") + try test(textRule: .atLeastOneLetter(message: ""), trueString: "1234 A", falseString: "1234") } func testDigitsOnly() throws { - try test(textRule: .digitsOnly, trueString: "1234", falseString: "1234 A") + try test(textRule: .digitsOnly(message: ""), trueString: "1234", falseString: "1234 A") } func testLettersOnly() throws { - try test(textRule: .lettersOnly, trueString: "Letters", falseString: "Digit 5") + try test(textRule: .lettersOnly(message: ""), trueString: "Letters", falseString: "Digit 5") } func testAtLeastOneLowercaseLetter() throws { - try test(textRule: .atLeastOneLowercaseLetter, trueString: "LOWEr", falseString: "UPPER") + try test(textRule: .atLeastOneLowercaseLetter(message: ""), trueString: "LOWEr", falseString: "UPPER") } func testAtLeastOneUppercaseLetter() throws { - try test(textRule: .atLeastOneUppercaseLetter, trueString: "Upper", falseString: "lower") + try test(textRule: .atLeastOneUppercaseLetter(message: ""), trueString: "Upper", falseString: "lower") } func testAtLeastOneSpecialCharacter() throws { - try test(textRule: .atLeastOneSpecialCharacter, trueString: "Special %", falseString: "No special") + try test(textRule: .atLeastOneSpecialCharacter(message: ""), trueString: "Special %", falseString: "No special") } func testNoSpecialCharacters() throws { - try test(textRule: .noSpecialCharacters, trueString: "No special", falseString: "Special %") + try test(textRule: .noSpecialCharacters(message: ""), trueString: "No special", falseString: "Special %") } func testEmail() throws { - try test(textRule: .email, trueString: "alievmaxx@gmail.com", falseString: "alievmaxx@.com") + try test(textRule: .email(message: ""), trueString: "alievmaxx@gmail.com", falseString: "alievmaxx@.com") } func testNotRecurringPincode() throws { - try test(textRule: .notRecurringPincode, trueString: "1234", falseString: "5555") + try test(textRule: .notRecurringPincode(message: ""), trueString: "1234", falseString: "5555") } func testRegex() throws { let dateRegex = "(\\d{2}).(\\d{2}).(\\d{4})" - try test(textRule: .regex(dateRegex), trueString: "21.12.2000", falseString: "21..2000") + try test(textRule: .regex(value: dateRegex, message: ""), trueString: "21.12.2000", falseString: "21..2000") } private func test(textRule: TextValidationRule, trueString: String, falseString: String) throws { diff --git a/Tests/FormViewTests/Validation/ValidatorTests.swift b/Tests/FormViewTests/Validation/ValidatorTests.swift index a2f5d59..949a68b 100644 --- a/Tests/FormViewTests/Validation/ValidatorTests.swift +++ b/Tests/FormViewTests/Validation/ValidatorTests.swift @@ -11,25 +11,22 @@ import XCTest final class ValidatorTests: XCTestCase { func testValidator() throws { - var text: String = "" var failedValidationRules: [TextValidationRule] = [] - let validator = FieldValidator( - value: Binding(get: { text }, set: { text = $0 }), - validationRules: [.digitsOnly], - inputRules: [.maxLength(4)], - failedValidationRules: Binding(get: { failedValidationRules }, set: { failedValidationRules = $0 }) + let validator = FieldValidator( + rules: [.digitsOnly(message: ""), .maxLength(count: 4, message: "")] ) - validator.value = "1" - validator.validate() + failedValidationRules = validator.validate(value: "1") XCTAssertTrue(failedValidationRules.isEmpty) + failedValidationRules.removeAll() - validator.value = "12_A" - XCTAssertEqual(failedValidationRules, [.digitsOnly]) + failedValidationRules = validator.validate(value: "12_A") + XCTAssertTrue(failedValidationRules.isEmpty == false) + failedValidationRules.removeAll() - validator.value = "12345" - let failedInputRules = validator.validateInput() - XCTAssertEqual(failedInputRules, [.maxLength(4)]) + failedValidationRules = validator.validate(value: "12345") + XCTAssertTrue(failedValidationRules.isEmpty == false) + failedValidationRules.removeAll() } } From eaa6f27a1352bd0b5e310af81b205c8b1bfd2b4c Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Tue, 11 Mar 2025 14:44:32 +0300 Subject: [PATCH 02/13] add example of validation from ViewModel --- .../InputFields/TextInputField.swift | 38 ++++++++- .../UI/ContentScreen/ContentView.swift | 10 ++- .../UI/ContentScreen/ContentViewModel.swift | 5 ++ README.md | 83 +++++++++++++++++++ 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/ExampleApp/ExampleApp/InputFields/TextInputField.swift b/ExampleApp/ExampleApp/InputFields/TextInputField.swift index 992299f..52aac1f 100644 --- a/ExampleApp/ExampleApp/InputFields/TextInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/TextInputField.swift @@ -8,17 +8,29 @@ import SwiftUI import FormView +enum OuterValidationRule { + case duplicateName + + var message: String { + switch self { + case .duplicateName: + return "This name already exists" + } + } +} + struct TextInputField: View { let title: LocalizedStringKey let text: Binding let failedRules: [TextValidationRule] + @Binding var outerRules: [OuterValidationRule] var body: some View { VStack(alignment: .leading) { TextField(title, text: text) .background(Color.white) - if failedRules.isEmpty == false { - Text(failedRules[0].message) + if let errorMessage = getErrorMessage() { + Text(errorMessage) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.red) } @@ -26,4 +38,26 @@ struct TextInputField: View { } .frame(height: 50) } + + private func getErrorMessage() -> String? { + if let message = failedRules.first?.message { + return message + } else if let message = outerRules.first?.message { + return message + } else { + return nil + } + } + + init( + title: LocalizedStringKey, + text: Binding, + failedRules: [TextValidationRule], + outerRules: Binding<[OuterValidationRule]> = .constant([]) + ) { + self.title = title + self.text = text + self.failedRules = failedRules + self._outerRules = outerRules + } } diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index c4513fa..62e9754 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -24,7 +24,12 @@ struct ContentView: View { .myRule ] ) { failedRules in - TextInputField(title: "Name", text: $viewModel.name, failedRules: failedRules) + TextInputField( + title: "Name", + text: $viewModel.name, + failedRules: failedRules, + outerRules: $viewModel.nameOuterRules + ) } FormField( value: $viewModel.age, @@ -57,6 +62,9 @@ struct ContentView: View { Button("Validate") { print("Form is valid: \(proxy.validate())") } + Button("Apply name outer rules") { + viewModel.applyNameOuterRules() + } } .padding(.horizontal, 16) .padding(.top, 40) diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift index a05e288..2f5d15f 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift @@ -12,6 +12,7 @@ class ContentViewModel: ObservableObject { @Published var age: String = "" @Published var pass: String = "" @Published var confirmPass: String = "" + @Published var nameOuterRules: [OuterValidationRule] = [] private let coordinator: ContentCoordinator @@ -20,6 +21,10 @@ class ContentViewModel: ObservableObject { print("init ContentViewModel") } + func applyNameOuterRules() { + nameOuterRules = [.duplicateName] + } + deinit { print("deinit ContentViewModel") } diff --git a/README.md b/README.md index 85ccdee..49f53ab 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,89 @@ A banch of predefind rules for text validation is available via `TextValidationR * equalTo - value equal to another value. Useful for password confirmation. * etc... +### Outer Validation Rules +If you need to display validation errors from external services (e.g., a backend), follow these steps: +1. Create an `OuterValidationRule` enum: +```swift +enum OuterValidationRule { + case duplicateName + + var message: String { + switch self { + case .duplicateName: + return "This name already exists" + } + } +} +``` + +2. Update the text field component: +```swift +struct TextInputField: View { + let title: LocalizedStringKey + let text: Binding + let failedRules: [TextValidationRule] + @Binding var outerRules: [OuterValidationRule] + + var body: some View { + VStack(alignment: .leading) { + TextField(title, text: text) + .background(Color.white) + if let errorMessage = getErrorMessage() { + Text(errorMessage) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.red) + } + Spacer() + } + .frame(height: 50) + } + + private func getErrorMessage() -> String? { + if let message = failedRules.first?.message { + return message + } else if let message = outerRules.first?.message { + return message + } else { + return nil + } + } + + init( + title: LocalizedStringKey, + text: Binding, + failedRules: [TextValidationRule], + outerRules: Binding<[OuterValidationRule]> = .constant([]) + ) { + self.title = title + self.text = text + self.failedRules = failedRules + self._outerRules = outerRules + } +} +``` +3. Update the text field initialization in your view: +```swift +TextInputField( + title: "Name", + text: $viewModel.name, + failedRules: failedRules, + outerRules: $viewModel.nameOuterRules +) +``` + +4. In your ViewModel, declare a `@Published` property of type `OuterValidationRule` and update its rules as needed: +```swift +class ContentViewModel: ObservableObject { + @Published var nameOuterRules: [OuterValidationRule] = [] + + func applyNameOuterRules() { + nameOuterRules = [.duplicateName] + } +} +``` + + ### Implementation Details FormView doesn't use any external dependencies. From 96219cc6674f40471d25c7f5dc866070dfd99e4a Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Tue, 11 Mar 2025 17:21:30 +0300 Subject: [PATCH 03/13] add error hiding on change of text --- ExampleApp/ExampleApp/InputFields/TextInputField.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ExampleApp/ExampleApp/InputFields/TextInputField.swift b/ExampleApp/ExampleApp/InputFields/TextInputField.swift index 52aac1f..dc730d5 100644 --- a/ExampleApp/ExampleApp/InputFields/TextInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/TextInputField.swift @@ -21,13 +21,13 @@ enum OuterValidationRule { struct TextInputField: View { let title: LocalizedStringKey - let text: Binding + @Binding var text: String let failedRules: [TextValidationRule] @Binding var outerRules: [OuterValidationRule] var body: some View { VStack(alignment: .leading) { - TextField(title, text: text) + TextField(title, text: $text) .background(Color.white) if let errorMessage = getErrorMessage() { Text(errorMessage) @@ -37,6 +37,9 @@ struct TextInputField: View { Spacer() } .frame(height: 50) + .onChange(of: text) { _ in + outerRules = [] + } } private func getErrorMessage() -> String? { @@ -56,7 +59,7 @@ struct TextInputField: View { outerRules: Binding<[OuterValidationRule]> = .constant([]) ) { self.title = title - self.text = text + self._text = text self.failedRules = failedRules self._outerRules = outerRules } From 58f9727ea38d0654b3cec0af9602568e75bdd941 Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Tue, 11 Mar 2025 17:22:51 +0300 Subject: [PATCH 04/13] update readme --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 49f53ab..cf76625 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,13 @@ enum OuterValidationRule { ```swift struct TextInputField: View { let title: LocalizedStringKey - let text: Binding + @Binding var text: String let failedRules: [TextValidationRule] @Binding var outerRules: [OuterValidationRule] var body: some View { VStack(alignment: .leading) { - TextField(title, text: text) + TextField(title, text: $text) .background(Color.white) if let errorMessage = getErrorMessage() { Text(errorMessage) @@ -160,6 +160,9 @@ struct TextInputField: View { Spacer() } .frame(height: 50) + .onChange(of: text) { _ in + outerRules = [] + } } private func getErrorMessage() -> String? { @@ -179,7 +182,7 @@ struct TextInputField: View { outerRules: Binding<[OuterValidationRule]> = .constant([]) ) { self.title = title - self.text = text + self._text = text self.failedRules = failedRules self._outerRules = outerRules } From 811f19ba6900df7df6f569f6c1e3b6b0a0aeacfb Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Tue, 11 Mar 2025 18:58:17 +0300 Subject: [PATCH 05/13] improvements after review --- Package.swift | 2 +- Tests/FormViewTests/Validation/ValidatorTests.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index d4ae6c4..80ff6cc 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( targets: ["FormView"]) ], dependencies: [ - .package(url: "https://github.com/nalexn/ViewInspector", exact: .init(0, 10, 1)) + .package(url: "https://github.com/nalexn/ViewInspector", exact: "0.10.1") ], targets: [ .target( diff --git a/Tests/FormViewTests/Validation/ValidatorTests.swift b/Tests/FormViewTests/Validation/ValidatorTests.swift index 949a68b..f921882 100644 --- a/Tests/FormViewTests/Validation/ValidatorTests.swift +++ b/Tests/FormViewTests/Validation/ValidatorTests.swift @@ -19,7 +19,6 @@ final class ValidatorTests: XCTestCase { failedValidationRules = validator.validate(value: "1") XCTAssertTrue(failedValidationRules.isEmpty) - failedValidationRules.removeAll() failedValidationRules = validator.validate(value: "12_A") XCTAssertTrue(failedValidationRules.isEmpty == false) From 83d506a4a1a777a0e1c7e50ede4b22d8d81e2c3e Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Thu, 13 Mar 2025 11:33:10 +0300 Subject: [PATCH 06/13] fixes after review --- Tests/FormViewTests/FormViewTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/FormViewTests/FormViewTests.swift b/Tests/FormViewTests/FormViewTests.swift index f95e4fc..f34ed81 100644 --- a/Tests/FormViewTests/FormViewTests.swift +++ b/Tests/FormViewTests/FormViewTests.swift @@ -88,12 +88,10 @@ final class FormViewTests: XCTestCase { let exp = sut.inspection.inspect { view in let scrollView = try view.find(ViewType.ScrollView.self) let textField1 = try view.find(viewWithId: 1).find(ViewType.TextField.self) -// let formField2 = try view.find(viewWithId: 2).view(FormField.self).actualView() try scrollView.callOnSubmit() try textField1.callOnChange(newValue: "field2", index: 1) -// XCTAssertEqual(formField2.focusField, "field2") XCTAssertTrue(true) } From d7793ee428e4b2732cd385109ad62587c79b9604 Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Thu, 13 Mar 2025 19:54:10 +0300 Subject: [PATCH 07/13] refactor --- .../xcshareddata/swiftpm/Package.resolved | 4 +- ExampleApp/ExampleApp/AppDelegate.swift | 2 - .../InputFields/SecureInputField.swift | 6 +- .../InputFields/TextInputField.swift | 23 +--- ExampleApp/ExampleApp/MyRule.swift | 6 +- .../UI/ContentScreen/ContentView.swift | 42 +++---- .../UI/ContentScreen/ContentViewModel.swift | 48 ++++++- Package.swift | 5 +- Sources/FormView/FormField.swift | 50 ++++---- Sources/FormView/FormView.swift | 15 ++- Sources/FormView/Preference/FieldState.swift | 2 +- .../Validation/Rules/TextValidationRule.swift | 117 ----------------- .../Validation/Rules/ValidationRule.swift | 118 +++++++++++++++++- .../Validators/FieldValidator.swift | 18 ++- .../Validation/Validators/FormValidator.swift | 6 +- Tests/FormViewTests/FormViewTests.swift | 118 ------------------ .../InspectionHelper/Inspection.swift | 23 ---- .../InspectionWrapperView.swift | 24 ---- .../Validation/TextValidationRuleTests.swift | 79 ------------ .../Validation/ValidatorTests.swift | 32 ----- 20 files changed, 240 insertions(+), 498 deletions(-) delete mode 100644 Sources/FormView/Validation/Rules/TextValidationRule.swift delete mode 100644 Tests/FormViewTests/FormViewTests.swift delete mode 100644 Tests/FormViewTests/InspectionHelper/Inspection.swift delete mode 100644 Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift delete mode 100644 Tests/FormViewTests/Validation/TextValidationRuleTests.swift delete mode 100644 Tests/FormViewTests/Validation/ValidatorTests.swift diff --git a/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index da2cfba..5868e83 100644 --- a/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nalexn/ViewInspector", "state" : { - "branch" : "master", - "revision" : "4effbd9143ab797eb60d2f32d4265c844c980946" + "revision" : "788e7879d38a839c4e348ab0762dcc0364e646a2", + "version" : "0.10.1" } } ], diff --git a/ExampleApp/ExampleApp/AppDelegate.swift b/ExampleApp/ExampleApp/AppDelegate.swift index 115016c..e6ff82c 100644 --- a/ExampleApp/ExampleApp/AppDelegate.swift +++ b/ExampleApp/ExampleApp/AppDelegate.swift @@ -13,8 +13,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - return true } } - diff --git a/ExampleApp/ExampleApp/InputFields/SecureInputField.swift b/ExampleApp/ExampleApp/InputFields/SecureInputField.swift index d821bcd..9ceddc3 100644 --- a/ExampleApp/ExampleApp/InputFields/SecureInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/SecureInputField.swift @@ -11,7 +11,7 @@ import FormView struct SecureInputField: View { let title: LocalizedStringKey let text: Binding - let failedRules: [TextValidationRule] + let failedRules: [ValidationRule] @FocusState private var isFocused: Bool @State private var isSecure = true @@ -24,8 +24,8 @@ struct SecureInputField: View { eyeImage } .background(Color.white) - if failedRules.isEmpty == false { - Text(failedRules[0].message) + if failedRules.isEmpty == false, let message = failedRules[0].message { + Text(message) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.red) } diff --git a/ExampleApp/ExampleApp/InputFields/TextInputField.swift b/ExampleApp/ExampleApp/InputFields/TextInputField.swift index dc730d5..08295c4 100644 --- a/ExampleApp/ExampleApp/InputFields/TextInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/TextInputField.swift @@ -8,22 +8,10 @@ import SwiftUI import FormView -enum OuterValidationRule { - case duplicateName - - var message: String { - switch self { - case .duplicateName: - return "This name already exists" - } - } -} - struct TextInputField: View { let title: LocalizedStringKey @Binding var text: String - let failedRules: [TextValidationRule] - @Binding var outerRules: [OuterValidationRule] + let failedRules: [ValidationRule] var body: some View { VStack(alignment: .leading) { @@ -37,16 +25,11 @@ struct TextInputField: View { Spacer() } .frame(height: 50) - .onChange(of: text) { _ in - outerRules = [] - } } private func getErrorMessage() -> String? { if let message = failedRules.first?.message { return message - } else if let message = outerRules.first?.message { - return message } else { return nil } @@ -55,12 +38,10 @@ struct TextInputField: View { init( title: LocalizedStringKey, text: Binding, - failedRules: [TextValidationRule], - outerRules: Binding<[OuterValidationRule]> = .constant([]) + failedRules: [ValidationRule] ) { self.title = title self._text = text self.failedRules = failedRules - self._outerRules = outerRules } } diff --git a/ExampleApp/ExampleApp/MyRule.swift b/ExampleApp/ExampleApp/MyRule.swift index 90577bc..12f745b 100644 --- a/ExampleApp/ExampleApp/MyRule.swift +++ b/ExampleApp/ExampleApp/MyRule.swift @@ -6,10 +6,10 @@ import FormView -extension TextValidationRule { +extension ValidationRule { static var myRule: Self { - TextValidationRule(message: "Shold contain T") { - $0.contains("T") + Self.custom { + return $0.contains("T") ? nil : "Shold contain T" } } } diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index 62e9754..f11d8da 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -13,58 +13,50 @@ struct ContentView: View { var body: some View { FormView( - validate: .never, + validate: .onFieldValueChanged, hideError: .onValueChanged ) { proxy in FormField( value: $viewModel.name, - rules: [ - TextValidationRule.noSpecialCharacters(message: "No spec chars"), - .notEmpty(message: "Name empty"), - .myRule - ] + rules: viewModel.nameValidationRules ) { failedRules in TextInputField( title: "Name", text: $viewModel.name, - failedRules: failedRules, - outerRules: $viewModel.nameOuterRules + failedRules: failedRules ) } + .disabled(viewModel.isLoading) FormField( value: $viewModel.age, - rules: [ - TextValidationRule.digitsOnly(message: "Digits only"), - .maxLength(count: 2, message: "Max length 2") - ] + rules: viewModel.ageValidationRules ) { failedRules in TextInputField(title: "Age", text: $viewModel.age, failedRules: failedRules) } + .disabled(viewModel.isLoading) FormField( value: $viewModel.pass, - rules: [ - TextValidationRule.atLeastOneDigit(message: "One digit"), - .atLeastOneLetter(message: "One letter"), - .notEmpty(message: "Pass not empty") - ] + rules: viewModel.passValidationRules ) { failedRules in SecureInputField(title: "Password", text: $viewModel.pass, failedRules: failedRules) } + .disabled(viewModel.isLoading) FormField( value: $viewModel.confirmPass, - rules: [ - TextValidationRule.equalTo(value: viewModel.pass, message: "Not equal to pass"), - .notEmpty(message: "Confirm pass not empty") - ] + rules: viewModel.confirmPassValidationRules ) { failedRules in SecureInputField(title: "Confirm Password", text: $viewModel.confirmPass, failedRules: failedRules) } - Button("Validate") { - print("Form is valid: \(proxy.validate())") + .disabled(viewModel.isLoading) + if viewModel.isLoading { + ProgressView() } - Button("Apply name outer rules") { - viewModel.applyNameOuterRules() + Button("Validate") { + Task { + print("Form is valid: \(await proxy.validate())") + } } + .disabled(viewModel.isLoading) } .padding(.horizontal, 16) .padding(.top, 40) diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift index 2f5d15f..be68ae0 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift @@ -6,23 +6,65 @@ // import SwiftUI +import FormView class ContentViewModel: ObservableObject { @Published var name: String = "" @Published var age: String = "" @Published var pass: String = "" @Published var confirmPass: String = "" - @Published var nameOuterRules: [OuterValidationRule] = [] + @Published var isLoading = false + + var nameValidationRules: [ValidationRule] = [] + var ageValidationRules: [ValidationRule] = [] + var passValidationRules: [ValidationRule] = [] + var confirmPassValidationRules: [ValidationRule] = [] private let coordinator: ContentCoordinator init(coordinator: ContentCoordinator) { self.coordinator = coordinator print("init ContentViewModel") + + nameValidationRules = [ + ValidationRule.notEmpty(message: "Name empty"), + ValidationRule.noSpecialCharacters(message: "No spec chars"), + ValidationRule.myRule, + ValidationRule.external { [weak self] in await self?.availabilityCheckAsync($0) } + ] + + ageValidationRules = [ + ValidationRule.digitsOnly(message: "Digits only"), + ValidationRule.maxLength(count: 2, message: "Max length 2") + ] + + passValidationRules = [ + ValidationRule.atLeastOneDigit(message: "One digit"), + ValidationRule.atLeastOneLetter(message: "One letter"), + ValidationRule.notEmpty(message: "Pass not empty") + ] + + confirmPassValidationRules = [ + ValidationRule.notEmpty(message: "Confirm pass not empty"), + ValidationRule.custom { [weak self] in + return $0 == self?.pass ? nil : "Not equal to pass" + } + ] } - func applyNameOuterRules() { - nameOuterRules = [.duplicateName] + @MainActor + private func availabilityCheckAsync(_ value: String) async -> String? { + print(#function) + + isLoading = true + + try? await Task.sleep(nanoseconds: 2_000_000_000) + + let isAvailable = Bool.random() + + isLoading = false + + return isAvailable ? nil : "Not available" } deinit { diff --git a/Package.swift b/Package.swift index d4ae6c4..2251412 100644 --- a/Package.swift +++ b/Package.swift @@ -19,9 +19,6 @@ let package = Package( targets: [ .target( name: "FormView", - dependencies: []), - .testTarget( - name: "FormViewTests", - dependencies: ["FormView", "ViewInspector"]) + dependencies: []) ] ) diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index a1e8d91..0fa9486 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -7,11 +7,11 @@ import SwiftUI -public struct FormField: View where Value == Rule.Value { - @Binding private var value: Value - @ViewBuilder private let content: ([Rule]) -> Content +public struct FormField: View { + @Binding private var value: String + @ViewBuilder private let content: ([ValidationRule]) -> Content - @State private var failedValidationRules: [Rule] = [] + @State private var failedValidationRules: [ValidationRule] = [] // Fields Focus @FocusState private var isFocused: Bool @@ -19,14 +19,14 @@ public struct FormField: V @Environment(\.focusedFieldId) var currentFocusedFieldId // ValidateInput - private let validator: FieldValidator + private let validator: FieldValidator @Environment(\.errorHideBehaviour) var errorHideBehaviour @Environment(\.validationBehaviour) var validationBehaviour public init( - value: Binding, - rules: [Rule] = [], - @ViewBuilder content: @escaping ([Rule]) -> Content + value: Binding, + rules: [ValidationRule] = [], + @ViewBuilder content: @escaping ([ValidationRule]) -> Content ) { self._value = value self.content = content @@ -46,7 +46,7 @@ public struct FormField: V value: [ // Замыкание для каждого филда вызывается FormValidator'ом из FormView для валидации по требованию FieldState(id: id, isFocused: isFocused) { - let failedRules = validator.validate(value: value) + let failedRules = await validator.validate(value: value, isNeedToCheckExternal: true) failedValidationRules = failedRules return failedRules.isEmpty @@ -57,23 +57,27 @@ public struct FormField: V // Fields Validation .onChange(of: value) { newValue in - if errorHideBehaviour == .onValueChanged { - failedValidationRules = .empty - } - - if validationBehaviour == .onFieldValueChanged { - failedValidationRules = validator.validate(value: newValue) + Task { @MainActor in + if errorHideBehaviour == .onValueChanged { + failedValidationRules = .empty + } + + if validationBehaviour == .onFieldValueChanged { + failedValidationRules = await validator.validate(value: newValue, isNeedToCheckExternal: false) + } } } .onChange(of: isFocused) { newValue in - if errorHideBehaviour == .onFocusLost && newValue == false { - failedValidationRules = .empty - } else if errorHideBehaviour == .onFocus && newValue == true { - failedValidationRules = .empty - } - - if validationBehaviour == .onFieldFocusLost && newValue == false { - failedValidationRules = validator.validate(value: value) + Task { @MainActor in + if errorHideBehaviour == .onFocusLost && newValue == false { + failedValidationRules = .empty + } else if errorHideBehaviour == .onFocus && newValue == true { + failedValidationRules = .empty + } + + if validationBehaviour == .onFieldFocusLost && newValue == false { + failedValidationRules = await validator.validate(value: value, isNeedToCheckExternal: false) + } } } } diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index 035c708..7599a59 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -31,19 +31,24 @@ private class FormStateHandler: ObservableObject { currentFocusedFieldId = focusedField?.id ?? .empty // Замыкание onValidateRun вызывается методом validate() FormValidator'a. - formValidator.onValidateRun = { [weak self] focusOnFirstFailedField in + formValidator.onValidateRun = { @MainActor [weak self] focusOnFirstFailedField in guard let self else { return false } - let resutls = newStates.map { $0.onValidate() } - + var results: [Bool] = [] + + for newState in newStates { + let result = await newState.onValidate() + results.append(result) + } + // Фокус на первом зафейленом филде. - if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { + if let index = results.firstIndex(of: false), focusOnFirstFailedField { currentFocusedFieldId = fieldStates[index].id } - return resutls.allSatisfy { $0 } + return results.allSatisfy { $0 } } } diff --git a/Sources/FormView/Preference/FieldState.swift b/Sources/FormView/Preference/FieldState.swift index 41b03c1..c71b427 100644 --- a/Sources/FormView/Preference/FieldState.swift +++ b/Sources/FormView/Preference/FieldState.swift @@ -10,7 +10,7 @@ import SwiftUI struct FieldState { var id: String var isFocused: Bool - var onValidate: () -> Bool + var onValidate: () async -> Bool } extension FieldState: Equatable { diff --git a/Sources/FormView/Validation/Rules/TextValidationRule.swift b/Sources/FormView/Validation/Rules/TextValidationRule.swift deleted file mode 100644 index 08b1a1d..0000000 --- a/Sources/FormView/Validation/Rules/TextValidationRule.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// TextValidationRule.swift -// -// -// Created by Maxim Aliev on 27.01.2023. -// - -import Foundation - -public struct TextValidationRule: ValidationRule { - public let message: String - - private let checkClosure: (String) -> Bool - - public init(message: String, checkClosure: @escaping (String) -> Bool) { - self.checkClosure = checkClosure - self.message = message - } - - public func check(value: String) -> Bool { - return checkClosure(value) - } -} - -extension TextValidationRule { - public static func notEmpty(message: String) -> Self { - TextValidationRule(message: message) { - $0.isEmpty == false - } - } - - public static func atLeastOneLowercaseLetter(message: String) -> Self { - TextValidationRule(message: message) { - $0.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil - } - } - - public static func atLeastOneUppercaseLetter(message: String) -> Self { - TextValidationRule(message: message) { - $0.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil - } - } - - public static func atLeastOneDigit(message: String) -> Self { - TextValidationRule(message: message) { - $0.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil - } - } - - public static func atLeastOneLetter(message: String) -> Self { - TextValidationRule(message: message) { - $0.rangeOfCharacter(from: CharacterSet.letters) != nil - } - } - - public static func digitsOnly(message: String) -> Self { - TextValidationRule(message: message) { - CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: $0)) - } - } - - public static func lettersOnly(message: String) -> Self { - TextValidationRule(message: message) { - CharacterSet.letters.isSuperset(of: CharacterSet(charactersIn: $0)) - } - } - - public static func atLeastOneSpecialCharacter(message: String) -> Self { - TextValidationRule(message: message) { - $0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) != nil - } - } - - public static func noSpecialCharacters(message: String) -> Self { - TextValidationRule(message: message) { - $0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) == nil - } - } - - public static func email(message: String) -> Self { - TextValidationRule(message: message) { - NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}") - .evaluate(with: $0) - } - } - - public static func notRecurringPincode(message: String) -> Self { - TextValidationRule(message: message) { - $0.range(of: "([0-9])\\1\\1\\1", options: .regularExpression) == nil - } - } - - public static func minLength(count: Int, message: String) -> Self { - TextValidationRule(message: message) { - $0.count >= count - } - } - - public static func maxLength(count: Int, message: String) -> Self { - TextValidationRule(message: message) { - $0.count <= count - } - } - - public static func regex(value: String, message: String) -> Self { - TextValidationRule(message: message) { - NSPredicate(format: "SELF MATCHES %@", value) - .evaluate(with: $0) - } - } - - public static func equalTo(value: String, message: String) -> Self { - TextValidationRule(message: message) { - $0 == value - } - } -} diff --git a/Sources/FormView/Validation/Rules/ValidationRule.swift b/Sources/FormView/Validation/Rules/ValidationRule.swift index 2019334..5f67b7f 100644 --- a/Sources/FormView/Validation/Rules/ValidationRule.swift +++ b/Sources/FormView/Validation/Rules/ValidationRule.swift @@ -2,13 +2,123 @@ // ValidationRule.swift // // -// Created by Maxim Aliev on 29.01.2023. +// Created by Maxim Aliev on 27.01.2023. // import Foundation +import SwiftUI -public protocol ValidationRule { - associatedtype Value +public class ValidationRule { + public var message: String? + public let isExternal: Bool - func check(value: Value) -> Bool + private let checkClosure: (String) async -> String? + + internal required init(isExternal: Bool, checkClosure: @escaping (String) async -> String?) { + self.checkClosure = checkClosure + self.isExternal = isExternal + } + + public func check(value: String) async -> Bool { + let message = await checkClosure(value) + self.message = message + + return message == nil + } +} + +extension ValidationRule { + public static func custom(checkClosure: @escaping (String) async -> String?) -> Self { + return Self(isExternal: false, checkClosure: checkClosure) + } + + public static func external(checkClosure: @escaping (String) async -> String?) -> Self { + return Self(isExternal: true, checkClosure: checkClosure) + } + + public static func notEmpty(message: String) -> Self { + return Self(isExternal: false) { + return $0.isEmpty == false ? nil : message + } + } + + public static func atLeastOneLowercaseLetter(message: String) -> Self { + return Self(isExternal: false) { + return $0.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil ? nil : message + } + } + + public static func atLeastOneUppercaseLetter(message: String) -> Self { + return Self(isExternal: false) { + $0.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil ? nil : message + } + } + + public static func atLeastOneDigit(message: String) -> Self { + return Self(isExternal: false) { + $0.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil ? nil : message + } + } + + public static func atLeastOneLetter(message: String) -> Self { + return Self(isExternal: false) { + $0.rangeOfCharacter(from: CharacterSet.letters) != nil ? nil : message + } + } + + public static func digitsOnly(message: String) -> Self { + return Self(isExternal: false) { + CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: $0)) ? nil : message + } + } + + public static func lettersOnly(message: String) -> Self { + return Self(isExternal: false) { + CharacterSet.letters.isSuperset(of: CharacterSet(charactersIn: $0)) ? nil : message + } + } + + public static func atLeastOneSpecialCharacter(message: String) -> Self { + return Self(isExternal: false) { + $0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) != nil ? nil : message + } + } + + public static func noSpecialCharacters(message: String) -> Self { + return Self(isExternal: false) { + $0.range(of: ".*[^A-Za-zА-Яа-яё0-9 ].*", options: .regularExpression) == nil ? nil : message + } + } + + public static func email(message: String) -> Self { + return Self(isExternal: false) { + NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}") + .evaluate(with: $0) ? nil : message + } + } + + public static func notRecurringPincode(message: String) -> Self { + return Self(isExternal: false) { + $0.range(of: "([0-9])\\1\\1\\1", options: .regularExpression) == nil ? nil : message + } + } + + public static func minLength(count: Int, message: String) -> Self { + return Self(isExternal: false) { + $0.count >= count ? nil : message + } + } + + public static func maxLength(count: Int, message: String) -> Self { + return Self(isExternal: false) { + $0.count <= count ? nil : message + } + } + + public static func regex(value: String, message: String) -> Self { + return Self(isExternal: false) { + NSPredicate(format: "SELF MATCHES %@", value) + .evaluate(with: $0) ? nil : message + } + } } diff --git a/Sources/FormView/Validation/Validators/FieldValidator.swift b/Sources/FormView/Validation/Validators/FieldValidator.swift index 36f1752..ec7d609 100644 --- a/Sources/FormView/Validation/Validators/FieldValidator.swift +++ b/Sources/FormView/Validation/Validators/FieldValidator.swift @@ -7,16 +7,22 @@ import SwiftUI -struct FieldValidator { - private let rules: [Rule] +struct FieldValidator { + private let rules: [ValidationRule] - init(rules: [Rule]) { + init(rules: [ValidationRule]) { self.rules = rules } - func validate(value: Rule.Value) -> [Rule] { - return rules.filter { - $0.check(value: value) == false + func validate(value: String, isNeedToCheckExternal: Bool) async -> [ValidationRule] { + var failedRules: [ValidationRule] = [] + + for rule in rules where rule.isExternal == false || isNeedToCheckExternal { + if await rule.check(value: value) == false { + failedRules.append(rule) + } } + + return failedRules } } diff --git a/Sources/FormView/Validation/Validators/FormValidator.swift b/Sources/FormView/Validation/Validators/FormValidator.swift index b493a70..25f775e 100644 --- a/Sources/FormView/Validation/Validators/FormValidator.swift +++ b/Sources/FormView/Validation/Validators/FormValidator.swift @@ -8,18 +8,18 @@ import Foundation public struct FormValidator { - var onValidateRun: ((Bool) -> Bool)? + var onValidateRun: ((Bool) async -> Bool)? public init() { onValidateRun = nil } - public func validate(focusOnFirstFailedField: Bool = true) -> Bool { + public func validate(focusOnFirstFailedField: Bool = true) async -> Bool { guard let onValidateRun else { assertionFailure("onValidateRun closure not found") return false } - return onValidateRun(focusOnFirstFailedField) + return await onValidateRun(focusOnFirstFailedField) } } diff --git a/Tests/FormViewTests/FormViewTests.swift b/Tests/FormViewTests/FormViewTests.swift deleted file mode 100644 index f95e4fc..0000000 --- a/Tests/FormViewTests/FormViewTests.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// FormViewTests.swift -// -// -// Created by Maxim Aliev on 18.02.2023. -// - -import SwiftUI -import XCTest -import ViewInspector -import Combine -@testable import FormView - -final class FormViewTests: XCTestCase { - @MainActor - func testPreventInvalidInput() throws { - var text1 = "" - var text2 = "" - let sut = InspectionWrapperView( - wrapped: FormView { _ in - ScrollView { - FormField( - value: Binding(get: { text1 }, set: { text1 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text1, text: Binding(get: { text1 }, set: { text1 = $0 })) - } - ) - .id(1) - FormField( - value: Binding(get: { text2 }, set: { text2 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text2, text: Binding(get: { text2 }, set: { text2 = $0 })) - } - ) - .id(2) - } - } - ) - - let exp = sut.inspection.inspect { view in - let scrollView = try view.find(ViewType.ScrollView.self) - let textField1 = try view.find(viewWithId: 1).find(ViewType.TextField.self) - - text1 = "123" - - try scrollView.callOnSubmit() - try textField1.callOnChange(newValue: "New Focus Field", index: 1) - try textField1.callOnChange(newValue: "123") - XCTAssertEqual(try textField1.input(), "123") - - try textField1.callOnChange(newValue: "123_A") - XCTAssertNotEqual(try textField1.input(), "123_A") - } - - ViewHosting.host(view: sut) - wait(for: [exp], timeout: 0.1) - } - - @MainActor - func testSubmitTextField() throws { - var text1 = "" - var text2 = "" - let sut = InspectionWrapperView( - wrapped: FormView { _ in - ScrollView { - FormField( - value: Binding(get: { text1 }, set: { text1 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text1, text: Binding(get: { text1 }, set: { text1 = $0 })) - } - ) - .id(1) - FormField( - value: Binding(get: { text2 }, set: { text2 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text2, text: Binding(get: { text2 }, set: { text2 = $0 })) - } - ) - .id(2) - } - } - ) - - let exp = sut.inspection.inspect { view in - let scrollView = try view.find(ViewType.ScrollView.self) - let textField1 = try view.find(viewWithId: 1).find(ViewType.TextField.self) -// let formField2 = try view.find(viewWithId: 2).view(FormField.self).actualView() - - try scrollView.callOnSubmit() - try textField1.callOnChange(newValue: "field2", index: 1) - -// XCTAssertEqual(formField2.focusField, "field2") - XCTAssertTrue(true) - } - - ViewHosting.host(view: sut.environment(\.focusedFieldId, "1")) - wait(for: [exp], timeout: 0.1) - } - - func testFocusNextField() throws { - let fieldStates = [ - FieldState(id: "1", isFocused: true, onValidate: { true }), - FieldState(id: "2", isFocused: false, onValidate: { false }) - ] - - var nextFocusField = FocusService.getNextFocusFieldId(states: fieldStates, currentFocusField: "1") - nextFocusField = nextFocusField.trimmingCharacters(in: .whitespaces) - XCTAssertEqual(nextFocusField, "2") - - nextFocusField = FocusService.getNextFocusFieldId(states: fieldStates, currentFocusField: "2") - nextFocusField = nextFocusField.trimmingCharacters(in: .whitespaces) - XCTAssertEqual(nextFocusField, "2") - } -} diff --git a/Tests/FormViewTests/InspectionHelper/Inspection.swift b/Tests/FormViewTests/InspectionHelper/Inspection.swift deleted file mode 100644 index 77b5b12..0000000 --- a/Tests/FormViewTests/InspectionHelper/Inspection.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Inspection.swift -// -// -// Created by Maxim Aliev on 18.02.2023. -// - -import Combine -import ViewInspector -@testable import FormView - -final class Inspection { - let notice = PassthroughSubject() - var callbacks: [UInt: (V) -> Void] = [:] - - func visit(_ view: V, _ line: UInt) { - if let callback = callbacks.removeValue(forKey: line) { - callback(view) - } - } -} - -extension Inspection: InspectionEmissary { } diff --git a/Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift b/Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift deleted file mode 100644 index 6fe6878..0000000 --- a/Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// InspectionWrapperView.swift -// -// -// Created by Maxim Aliev on 18.02.2023. -// - -import SwiftUI - -struct InspectionWrapperView: View { - let inspection = Inspection() - var wrapped: V - - init(wrapped: V) { - self.wrapped = wrapped - } - - var body: some View { - wrapped - .onReceive(inspection.notice) { - inspection.visit(self, $0) - } - } -} diff --git a/Tests/FormViewTests/Validation/TextValidationRuleTests.swift b/Tests/FormViewTests/Validation/TextValidationRuleTests.swift deleted file mode 100644 index a4e2569..0000000 --- a/Tests/FormViewTests/Validation/TextValidationRuleTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// TextValidationRuleTests.swift -// -// -// Created by Maxim Aliev on 07.02.2023. -// - -import XCTest -@testable import FormView - -final class TextValidationRuleTests: XCTestCase { - func testIgnoreEmpty() throws { - try test(textRule: .digitsOnly(message: ""), trueString: "", falseString: "1234 A") - } - - func testNotEmpty() throws { - try test(textRule: .notEmpty(message: ""), trueString: "Not empty", falseString: "") - } - - func testMinLength() throws { - try test(textRule: .minLength(count: 4, message: ""), trueString: "1234", falseString: "123") - } - - func testMaxLength() throws { - try test(textRule: .maxLength(count: 4, message: ""), trueString: "1234", falseString: "123456") - } - - func testAtLeastOneDigit() throws { - try test(textRule: .atLeastOneDigit(message: ""), trueString: "Digit 5", falseString: "No Digits") - } - - func testAtLeastOneLetter() throws { - try test(textRule: .atLeastOneLetter(message: ""), trueString: "1234 A", falseString: "1234") - } - - func testDigitsOnly() throws { - try test(textRule: .digitsOnly(message: ""), trueString: "1234", falseString: "1234 A") - } - - func testLettersOnly() throws { - try test(textRule: .lettersOnly(message: ""), trueString: "Letters", falseString: "Digit 5") - } - - func testAtLeastOneLowercaseLetter() throws { - try test(textRule: .atLeastOneLowercaseLetter(message: ""), trueString: "LOWEr", falseString: "UPPER") - } - - func testAtLeastOneUppercaseLetter() throws { - try test(textRule: .atLeastOneUppercaseLetter(message: ""), trueString: "Upper", falseString: "lower") - } - - func testAtLeastOneSpecialCharacter() throws { - try test(textRule: .atLeastOneSpecialCharacter(message: ""), trueString: "Special %", falseString: "No special") - } - - func testNoSpecialCharacters() throws { - try test(textRule: .noSpecialCharacters(message: ""), trueString: "No special", falseString: "Special %") - } - - func testEmail() throws { - try test(textRule: .email(message: ""), trueString: "alievmaxx@gmail.com", falseString: "alievmaxx@.com") - } - - func testNotRecurringPincode() throws { - try test(textRule: .notRecurringPincode(message: ""), trueString: "1234", falseString: "5555") - } - - func testRegex() throws { - let dateRegex = "(\\d{2}).(\\d{2}).(\\d{4})" - try test(textRule: .regex(value: dateRegex, message: ""), trueString: "21.12.2000", falseString: "21..2000") - } - - private func test(textRule: TextValidationRule, trueString: String, falseString: String) throws { - let isPassed = textRule.check(value: trueString) - let isFailed = textRule.check(value: falseString) == false - - XCTAssertTrue(isPassed && isFailed) - } -} diff --git a/Tests/FormViewTests/Validation/ValidatorTests.swift b/Tests/FormViewTests/Validation/ValidatorTests.swift deleted file mode 100644 index 949a68b..0000000 --- a/Tests/FormViewTests/Validation/ValidatorTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ValidatorTests.swift -// -// -// Created by Maxim Aliev on 19.02.2023. -// - -import SwiftUI -import XCTest -@testable import FormView - -final class ValidatorTests: XCTestCase { - func testValidator() throws { - var failedValidationRules: [TextValidationRule] = [] - - let validator = FieldValidator( - rules: [.digitsOnly(message: ""), .maxLength(count: 4, message: "")] - ) - - failedValidationRules = validator.validate(value: "1") - XCTAssertTrue(failedValidationRules.isEmpty) - failedValidationRules.removeAll() - - failedValidationRules = validator.validate(value: "12_A") - XCTAssertTrue(failedValidationRules.isEmpty == false) - failedValidationRules.removeAll() - - failedValidationRules = validator.validate(value: "12345") - XCTAssertTrue(failedValidationRules.isEmpty == false) - failedValidationRules.removeAll() - } -} From 24e3cc380f430899b5b9c2cb620c96472873a586 Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Thu, 13 Mar 2025 19:56:57 +0300 Subject: [PATCH 08/13] refactor --- .../ExampleApp/InputFields/TextInputField.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ExampleApp/ExampleApp/InputFields/TextInputField.swift b/ExampleApp/ExampleApp/InputFields/TextInputField.swift index 08295c4..0ff8ef4 100644 --- a/ExampleApp/ExampleApp/InputFields/TextInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/TextInputField.swift @@ -27,14 +27,6 @@ struct TextInputField: View { .frame(height: 50) } - private func getErrorMessage() -> String? { - if let message = failedRules.first?.message { - return message - } else { - return nil - } - } - init( title: LocalizedStringKey, text: Binding, @@ -44,4 +36,12 @@ struct TextInputField: View { self._text = text self.failedRules = failedRules } + + private func getErrorMessage() -> String? { + if let message = failedRules.first?.message { + return message + } else { + return nil + } + } } From 15bf9670432d0c13ceaa17acdf2844e9b5302a5d Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Thu, 13 Mar 2025 19:58:59 +0300 Subject: [PATCH 09/13] delete tests --- .../xcshareddata/swiftpm/Package.resolved | 4 +- Package.swift | 5 +- Tests/FormViewTests/FormViewTests.swift | 116 ------------------ .../InspectionHelper/Inspection.swift | 23 ---- .../InspectionWrapperView.swift | 24 ---- .../Validation/TextValidationRuleTests.swift | 79 ------------ .../Validation/ValidatorTests.swift | 31 ----- 7 files changed, 3 insertions(+), 279 deletions(-) delete mode 100644 Tests/FormViewTests/FormViewTests.swift delete mode 100644 Tests/FormViewTests/InspectionHelper/Inspection.swift delete mode 100644 Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift delete mode 100644 Tests/FormViewTests/Validation/TextValidationRuleTests.swift delete mode 100644 Tests/FormViewTests/Validation/ValidatorTests.swift diff --git a/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index da2cfba..5868e83 100644 --- a/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nalexn/ViewInspector", "state" : { - "branch" : "master", - "revision" : "4effbd9143ab797eb60d2f32d4265c844c980946" + "revision" : "788e7879d38a839c4e348ab0762dcc0364e646a2", + "version" : "0.10.1" } } ], diff --git a/Package.swift b/Package.swift index 80ff6cc..6525ca5 100644 --- a/Package.swift +++ b/Package.swift @@ -19,9 +19,6 @@ let package = Package( targets: [ .target( name: "FormView", - dependencies: []), - .testTarget( - name: "FormViewTests", - dependencies: ["FormView", "ViewInspector"]) + dependencies: []) ] ) diff --git a/Tests/FormViewTests/FormViewTests.swift b/Tests/FormViewTests/FormViewTests.swift deleted file mode 100644 index f34ed81..0000000 --- a/Tests/FormViewTests/FormViewTests.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// FormViewTests.swift -// -// -// Created by Maxim Aliev on 18.02.2023. -// - -import SwiftUI -import XCTest -import ViewInspector -import Combine -@testable import FormView - -final class FormViewTests: XCTestCase { - @MainActor - func testPreventInvalidInput() throws { - var text1 = "" - var text2 = "" - let sut = InspectionWrapperView( - wrapped: FormView { _ in - ScrollView { - FormField( - value: Binding(get: { text1 }, set: { text1 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text1, text: Binding(get: { text1 }, set: { text1 = $0 })) - } - ) - .id(1) - FormField( - value: Binding(get: { text2 }, set: { text2 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text2, text: Binding(get: { text2 }, set: { text2 = $0 })) - } - ) - .id(2) - } - } - ) - - let exp = sut.inspection.inspect { view in - let scrollView = try view.find(ViewType.ScrollView.self) - let textField1 = try view.find(viewWithId: 1).find(ViewType.TextField.self) - - text1 = "123" - - try scrollView.callOnSubmit() - try textField1.callOnChange(newValue: "New Focus Field", index: 1) - try textField1.callOnChange(newValue: "123") - XCTAssertEqual(try textField1.input(), "123") - - try textField1.callOnChange(newValue: "123_A") - XCTAssertNotEqual(try textField1.input(), "123_A") - } - - ViewHosting.host(view: sut) - wait(for: [exp], timeout: 0.1) - } - - @MainActor - func testSubmitTextField() throws { - var text1 = "" - var text2 = "" - let sut = InspectionWrapperView( - wrapped: FormView { _ in - ScrollView { - FormField( - value: Binding(get: { text1 }, set: { text1 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text1, text: Binding(get: { text1 }, set: { text1 = $0 })) - } - ) - .id(1) - FormField( - value: Binding(get: { text2 }, set: { text2 = $0 }), - rules: [TextValidationRule.digitsOnly(message: "")], - content: { _ in - TextField(text2, text: Binding(get: { text2 }, set: { text2 = $0 })) - } - ) - .id(2) - } - } - ) - - let exp = sut.inspection.inspect { view in - let scrollView = try view.find(ViewType.ScrollView.self) - let textField1 = try view.find(viewWithId: 1).find(ViewType.TextField.self) - - try scrollView.callOnSubmit() - try textField1.callOnChange(newValue: "field2", index: 1) - - XCTAssertTrue(true) - } - - ViewHosting.host(view: sut.environment(\.focusedFieldId, "1")) - wait(for: [exp], timeout: 0.1) - } - - func testFocusNextField() throws { - let fieldStates = [ - FieldState(id: "1", isFocused: true, onValidate: { true }), - FieldState(id: "2", isFocused: false, onValidate: { false }) - ] - - var nextFocusField = FocusService.getNextFocusFieldId(states: fieldStates, currentFocusField: "1") - nextFocusField = nextFocusField.trimmingCharacters(in: .whitespaces) - XCTAssertEqual(nextFocusField, "2") - - nextFocusField = FocusService.getNextFocusFieldId(states: fieldStates, currentFocusField: "2") - nextFocusField = nextFocusField.trimmingCharacters(in: .whitespaces) - XCTAssertEqual(nextFocusField, "2") - } -} diff --git a/Tests/FormViewTests/InspectionHelper/Inspection.swift b/Tests/FormViewTests/InspectionHelper/Inspection.swift deleted file mode 100644 index 77b5b12..0000000 --- a/Tests/FormViewTests/InspectionHelper/Inspection.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Inspection.swift -// -// -// Created by Maxim Aliev on 18.02.2023. -// - -import Combine -import ViewInspector -@testable import FormView - -final class Inspection { - let notice = PassthroughSubject() - var callbacks: [UInt: (V) -> Void] = [:] - - func visit(_ view: V, _ line: UInt) { - if let callback = callbacks.removeValue(forKey: line) { - callback(view) - } - } -} - -extension Inspection: InspectionEmissary { } diff --git a/Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift b/Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift deleted file mode 100644 index 6fe6878..0000000 --- a/Tests/FormViewTests/InspectionHelper/InspectionWrapperView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// InspectionWrapperView.swift -// -// -// Created by Maxim Aliev on 18.02.2023. -// - -import SwiftUI - -struct InspectionWrapperView: View { - let inspection = Inspection() - var wrapped: V - - init(wrapped: V) { - self.wrapped = wrapped - } - - var body: some View { - wrapped - .onReceive(inspection.notice) { - inspection.visit(self, $0) - } - } -} diff --git a/Tests/FormViewTests/Validation/TextValidationRuleTests.swift b/Tests/FormViewTests/Validation/TextValidationRuleTests.swift deleted file mode 100644 index a4e2569..0000000 --- a/Tests/FormViewTests/Validation/TextValidationRuleTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// TextValidationRuleTests.swift -// -// -// Created by Maxim Aliev on 07.02.2023. -// - -import XCTest -@testable import FormView - -final class TextValidationRuleTests: XCTestCase { - func testIgnoreEmpty() throws { - try test(textRule: .digitsOnly(message: ""), trueString: "", falseString: "1234 A") - } - - func testNotEmpty() throws { - try test(textRule: .notEmpty(message: ""), trueString: "Not empty", falseString: "") - } - - func testMinLength() throws { - try test(textRule: .minLength(count: 4, message: ""), trueString: "1234", falseString: "123") - } - - func testMaxLength() throws { - try test(textRule: .maxLength(count: 4, message: ""), trueString: "1234", falseString: "123456") - } - - func testAtLeastOneDigit() throws { - try test(textRule: .atLeastOneDigit(message: ""), trueString: "Digit 5", falseString: "No Digits") - } - - func testAtLeastOneLetter() throws { - try test(textRule: .atLeastOneLetter(message: ""), trueString: "1234 A", falseString: "1234") - } - - func testDigitsOnly() throws { - try test(textRule: .digitsOnly(message: ""), trueString: "1234", falseString: "1234 A") - } - - func testLettersOnly() throws { - try test(textRule: .lettersOnly(message: ""), trueString: "Letters", falseString: "Digit 5") - } - - func testAtLeastOneLowercaseLetter() throws { - try test(textRule: .atLeastOneLowercaseLetter(message: ""), trueString: "LOWEr", falseString: "UPPER") - } - - func testAtLeastOneUppercaseLetter() throws { - try test(textRule: .atLeastOneUppercaseLetter(message: ""), trueString: "Upper", falseString: "lower") - } - - func testAtLeastOneSpecialCharacter() throws { - try test(textRule: .atLeastOneSpecialCharacter(message: ""), trueString: "Special %", falseString: "No special") - } - - func testNoSpecialCharacters() throws { - try test(textRule: .noSpecialCharacters(message: ""), trueString: "No special", falseString: "Special %") - } - - func testEmail() throws { - try test(textRule: .email(message: ""), trueString: "alievmaxx@gmail.com", falseString: "alievmaxx@.com") - } - - func testNotRecurringPincode() throws { - try test(textRule: .notRecurringPincode(message: ""), trueString: "1234", falseString: "5555") - } - - func testRegex() throws { - let dateRegex = "(\\d{2}).(\\d{2}).(\\d{4})" - try test(textRule: .regex(value: dateRegex, message: ""), trueString: "21.12.2000", falseString: "21..2000") - } - - private func test(textRule: TextValidationRule, trueString: String, falseString: String) throws { - let isPassed = textRule.check(value: trueString) - let isFailed = textRule.check(value: falseString) == false - - XCTAssertTrue(isPassed && isFailed) - } -} diff --git a/Tests/FormViewTests/Validation/ValidatorTests.swift b/Tests/FormViewTests/Validation/ValidatorTests.swift deleted file mode 100644 index f921882..0000000 --- a/Tests/FormViewTests/Validation/ValidatorTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ValidatorTests.swift -// -// -// Created by Maxim Aliev on 19.02.2023. -// - -import SwiftUI -import XCTest -@testable import FormView - -final class ValidatorTests: XCTestCase { - func testValidator() throws { - var failedValidationRules: [TextValidationRule] = [] - - let validator = FieldValidator( - rules: [.digitsOnly(message: ""), .maxLength(count: 4, message: "")] - ) - - failedValidationRules = validator.validate(value: "1") - XCTAssertTrue(failedValidationRules.isEmpty) - - failedValidationRules = validator.validate(value: "12_A") - XCTAssertTrue(failedValidationRules.isEmpty == false) - failedValidationRules.removeAll() - - failedValidationRules = validator.validate(value: "12345") - XCTAssertTrue(failedValidationRules.isEmpty == false) - failedValidationRules.removeAll() - } -} From 54e242aae8d54a9b242c28560371a11bcf734400 Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Thu, 13 Mar 2025 20:04:11 +0300 Subject: [PATCH 10/13] update gitignore --- .gitignore | 1 + Package.resolved | 14 -------------- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 Package.resolved diff --git a/.gitignore b/.gitignore index 3b29812..c5f2894 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +Package.resolved* \ No newline at end of file diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 5868e83..0000000 --- a/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "viewinspector", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nalexn/ViewInspector", - "state" : { - "revision" : "788e7879d38a839c4e348ab0762dcc0364e646a2", - "version" : "0.10.1" - } - } - ], - "version" : 2 -} From 3cfca6b0caff850e8d214a067c5d979949bdbaff Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Fri, 14 Mar 2025 13:15:31 +0300 Subject: [PATCH 11/13] fixes after review --- ExampleApp/ExampleApp/InputFields/TextInputField.swift | 10 +--------- ExampleApp/ExampleApp/MyRule.swift | 2 +- .../ExampleApp/UI/ContentScreen/ContentView.swift | 6 +----- .../ExampleApp/UI/ContentScreen/ContentViewModel.swift | 4 ++++ Sources/FormView/FormView.swift | 2 +- 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/ExampleApp/ExampleApp/InputFields/TextInputField.swift b/ExampleApp/ExampleApp/InputFields/TextInputField.swift index 0ff8ef4..d39d1a4 100644 --- a/ExampleApp/ExampleApp/InputFields/TextInputField.swift +++ b/ExampleApp/ExampleApp/InputFields/TextInputField.swift @@ -17,7 +17,7 @@ struct TextInputField: View { VStack(alignment: .leading) { TextField(title, text: $text) .background(Color.white) - if let errorMessage = getErrorMessage() { + if let errorMessage = failedRules.first?.message { Text(errorMessage) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.red) @@ -36,12 +36,4 @@ struct TextInputField: View { self._text = text self.failedRules = failedRules } - - private func getErrorMessage() -> String? { - if let message = failedRules.first?.message { - return message - } else { - return nil - } - } } diff --git a/ExampleApp/ExampleApp/MyRule.swift b/ExampleApp/ExampleApp/MyRule.swift index 12f745b..1372ce1 100644 --- a/ExampleApp/ExampleApp/MyRule.swift +++ b/ExampleApp/ExampleApp/MyRule.swift @@ -9,7 +9,7 @@ import FormView extension ValidationRule { static var myRule: Self { Self.custom { - return $0.contains("T") ? nil : "Shold contain T" + return $0.contains("T") ? nil : "Should contain T" } } } diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift index f11d8da..9e0427c 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift @@ -20,11 +20,7 @@ struct ContentView: View { value: $viewModel.name, rules: viewModel.nameValidationRules ) { failedRules in - TextInputField( - title: "Name", - text: $viewModel.name, - failedRules: failedRules - ) + TextInputField(title: "Name", text: $viewModel.name, failedRules: failedRules) } .disabled(viewModel.isLoading) FormField( diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift index be68ae0..fcce647 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift @@ -26,6 +26,10 @@ class ContentViewModel: ObservableObject { self.coordinator = coordinator print("init ContentViewModel") + setupValidationRules() + } + + private func setupValidationRules() { nameValidationRules = [ ValidationRule.notEmpty(message: "Name empty"), ValidationRule.noSpecialCharacters(message: "No spec chars"), diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index 7599a59..a33fa83 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -42,7 +42,7 @@ private class FormStateHandler: ObservableObject { let result = await newState.onValidate() results.append(result) } - + // Фокус на первом зафейленом филде. if let index = results.firstIndex(of: false), focusOnFirstFailedField { currentFocusedFieldId = fieldStates[index].id From f1055ff5f48b1967f6f03c33731aaee842633bea Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Mon, 17 Mar 2025 19:31:16 +0300 Subject: [PATCH 12/13] update gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c5f2894..d4f691c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc -Package.resolved* \ No newline at end of file +Package.resolved* +*Package.resolved From 649c62b5057858807a5ed45243305ff99e1246fa Mon Sep 17 00:00:00 2001 From: Yan Boyko Date: Mon, 17 Mar 2025 19:35:00 +0300 Subject: [PATCH 13/13] fix comments --- .gitignore | 39 +++++++++++++++---- .../xcshareddata/swiftpm/Package.resolved | 14 ------- Package.swift | 6 ++- 3 files changed, 35 insertions(+), 24 deletions(-) delete mode 100644 ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/.gitignore b/.gitignore index d4f691c..cf206b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,34 @@ +*.DS_Store .DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ +*.generated.swift + +## Build generated +build/ DerivedData/ -.swiftpm/config/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc -Package.resolved* +xcuserdata/ +xcshareddata/ +/.swiftpm +*.xcuserstate + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +# Swift Package Manager +.build/ *Package.resolved +Package.resolved* + +# SwiftLint +lintOutput.json diff --git a/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 5868e83..0000000 --- a/ExampleApp/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "viewinspector", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nalexn/ViewInspector", - "state" : { - "revision" : "788e7879d38a839c4e348ab0762dcc0364e646a2", - "version" : "0.10.1" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index 6525ca5..cb3a288 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,8 @@ let package = Package( products: [ .library( name: "FormView", - targets: ["FormView"]) + targets: ["FormView"] + ) ], dependencies: [ .package(url: "https://github.com/nalexn/ViewInspector", exact: "0.10.1") @@ -19,6 +20,7 @@ let package = Package( targets: [ .target( name: "FormView", - dependencies: []) + dependencies: [] + ) ] )