From ddf55cb91c283a5bbdd9bcfc7a47b545f3ada7be Mon Sep 17 00:00:00 2001 From: Kuniwak Date: Fri, 13 Jul 2018 00:38:22 +0900 Subject: [PATCH 1/3] Update to Swift 4.2 --- Cartfile.resolved | 4 ++-- Carthage/Checkouts/PromiseKit | 2 +- Carthage/Checkouts/RxSwift | 2 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++++++ 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 TestableDesignExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Cartfile.resolved b/Cartfile.resolved index e4cae3e..629cae0 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,6 +1,6 @@ github "JohnSundell/Unbox" "2.5.0" github "Kuniwak/MirrorDiffKit" "2.0.0" -github "ReactiveX/RxSwift" "f043778214c8f182018ccdfbf7f440edbe0aecc8" +github "ReactiveX/RxSwift" "53cd723d40d05177e790c8c34c36cec7092a6106" github "antitypical/Result" "3.2.4" github "mac-cain13/R.swift.Library" "v4.0.0" -github "mxcl/PromiseKit" "4.5.0" +github "mxcl/PromiseKit" "4.5.2" diff --git a/Carthage/Checkouts/PromiseKit b/Carthage/Checkouts/PromiseKit index 6bab5e0..c70677a 160000 --- a/Carthage/Checkouts/PromiseKit +++ b/Carthage/Checkouts/PromiseKit @@ -1 +1 @@ -Subproject commit 6bab5e0c7f93947d9c0a7df0937add7454657f2c +Subproject commit c70677a12b8367a808af7049a94096370f7cda0d diff --git a/Carthage/Checkouts/RxSwift b/Carthage/Checkouts/RxSwift index f043778..53cd723 160000 --- a/Carthage/Checkouts/RxSwift +++ b/Carthage/Checkouts/RxSwift @@ -1 +1 @@ -Subproject commit f043778214c8f182018ccdfbf7f440edbe0aecc8 +Subproject commit 53cd723d40d05177e790c8c34c36cec7092a6106 diff --git a/TestableDesignExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/TestableDesignExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/TestableDesignExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + From bc7108d914b988d4eef19b73335567ef8a57f260 Mon Sep 17 00:00:00 2001 From: Kuniwak Date: Wed, 26 Sep 2018 05:41:41 +0900 Subject: [PATCH 2/3] Update RxSwift --- Cartfile.resolved | 2 +- Carthage/Checkouts/RxSwift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cartfile.resolved b/Cartfile.resolved index 629cae0..68c3e10 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,6 +1,6 @@ github "JohnSundell/Unbox" "2.5.0" github "Kuniwak/MirrorDiffKit" "2.0.0" -github "ReactiveX/RxSwift" "53cd723d40d05177e790c8c34c36cec7092a6106" +github "ReactiveX/RxSwift" "0df62b4d562f8620d4b795b18e4adf0b631527a1" github "antitypical/Result" "3.2.4" github "mac-cain13/R.swift.Library" "v4.0.0" github "mxcl/PromiseKit" "4.5.2" diff --git a/Carthage/Checkouts/RxSwift b/Carthage/Checkouts/RxSwift index 53cd723..0df62b4 160000 --- a/Carthage/Checkouts/RxSwift +++ b/Carthage/Checkouts/RxSwift @@ -1 +1 @@ -Subproject commit 53cd723d40d05177e790c8c34c36cec7092a6106 +Subproject commit 0df62b4d562f8620d4b795b18e4adf0b631527a1 From b3eb83e4219ed79db40a516fa4bc906d326cf17b Mon Sep 17 00:00:00 2001 From: Kuniwak Date: Wed, 26 Sep 2018 05:41:54 +0900 Subject: [PATCH 3/3] Implement examples for validation --- .idea/TestableDesignExample.iml | 2 + .idea/codeStyles/Project.xml | 32 +++ .idea/misc.xml | 6 + .idea/modules.xml | 8 + .../TestableDesignExample.xml | 8 + .../TestableDesignExampleTests.xml | 7 + .../TestableDesignExampleUITests.xml | 5 + .idea/vcs.xml | 33 +++ .idea/xcode.xml | 4 + .../project.pbxproj | 108 +++++++++- .../TestableDesignExampleTests.xcscheme | 41 ++++ .../Shared/Validation/Validation.swift | 32 +++ .../Shared/Validation/ValidationResult.swift | 49 +++++ .../ExampleValidationComposer.swift | 6 + .../Model/ExampleAccount.Draft.swift | 11 + .../Validation/Model/ExampleAccount.swift | 14 ++ .../Model/ExampleAccount.validate.swift | 156 ++++++++++++++ .../Model/ExampleAccount.validateTests.swift | 192 ++++++++++++++++++ .../Model/ExampleAccountFactory.swift | 33 +++ .../Model/ExampleValidationModel.swift | 49 +++++ .../Model/ExampleValidationModelTests.swift | 80 ++++++++ .../ExampleValidationScreenRootView.swift | 35 ++++ .../ExampleValidationScreenRootView.xib | 132 ++++++++++++ .../ExampleValidationViewBinding.swift | 83 ++++++++ TestableDesignExample/Resources/Color.swift | 32 +++ 25 files changed, 1156 insertions(+), 2 deletions(-) create mode 100644 .idea/TestableDesignExample.iml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations/TestableDesignExample.xml create mode 100644 .idea/runConfigurations/TestableDesignExampleTests.xml create mode 100644 .idea/runConfigurations/TestableDesignExampleUITests.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/xcode.xml create mode 100644 TestableDesignExample.xcodeproj/xcshareddata/xcschemes/TestableDesignExampleTests.xcscheme create mode 100644 TestableDesignExample/MvcArchitecture/Shared/Validation/Validation.swift create mode 100644 TestableDesignExample/MvcArchitecture/Shared/Validation/ValidationResult.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/ExampleValidationComposer.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.Draft.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validate.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validateTests.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccountFactory.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModel.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModelTests.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.swift create mode 100644 TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.xib create mode 100644 TestableDesignExample/MvcArchitecture/Validation/View/ViewBinding/ExampleValidationViewBinding.swift create mode 100644 TestableDesignExample/Resources/Color.swift diff --git a/.idea/TestableDesignExample.iml b/.idea/TestableDesignExample.iml new file mode 100644 index 0000000..74121dc --- /dev/null +++ b/.idea/TestableDesignExample.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..45f86bc --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..28a804d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..266061a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/TestableDesignExample.xml b/.idea/runConfigurations/TestableDesignExample.xml new file mode 100644 index 0000000..57fd333 --- /dev/null +++ b/.idea/runConfigurations/TestableDesignExample.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/TestableDesignExampleTests.xml b/.idea/runConfigurations/TestableDesignExampleTests.xml new file mode 100644 index 0000000..7f14e37 --- /dev/null +++ b/.idea/runConfigurations/TestableDesignExampleTests.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/TestableDesignExampleUITests.xml b/.idea/runConfigurations/TestableDesignExampleUITests.xml new file mode 100644 index 0000000..880cf24 --- /dev/null +++ b/.idea/runConfigurations/TestableDesignExampleUITests.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..ac682ef --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/xcode.xml b/.idea/xcode.xml new file mode 100644 index 0000000..bd83f48 --- /dev/null +++ b/.idea/xcode.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TestableDesignExample.xcodeproj/project.pbxproj b/TestableDesignExample.xcodeproj/project.pbxproj index b2b15ef..6e71b7d 100644 --- a/TestableDesignExample.xcodeproj/project.pbxproj +++ b/TestableDesignExample.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 2475E423E64E77274E37BBF2 /* StargazersInfiniteScrollControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E4A20AF63001833F7647 /* StargazersInfiniteScrollControllerTests.swift */; }; 2475E4258308ED2D3361DB75 /* UserRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E44FF94942A600769656 /* UserRepositoryTests.swift */; }; 2475E46194E77FB962FFBD72 /* UserScreenRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E3BBDF2F0CD7483117FD /* UserScreenRootView.swift */; }; + 2475E485404E14B466A00D45 /* ExampleAccount.Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E97FBFA43244E70A138D /* ExampleAccount.Draft.swift */; }; 2475E48B01B8DDA59B303872 /* ScrollViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E2881D2B2457DC9309CB /* ScrollViewFactory.swift */; }; 2475E493073E5E0575023263 /* UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E917CEDA9911B0F9A54E /* UserModel.swift */; }; 2475E4B7ACC7D275F796758F /* UrlOpenerStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E40BE1B2CE78FAF70D47 /* UrlOpenerStub.swift */; }; @@ -64,6 +65,7 @@ 2475E6B24A535292DA644CA9 /* StargazersInfiniteScrollController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E55F14C73701A5DD8DD0 /* StargazersInfiniteScrollController.swift */; }; 2475E6DDE748544827215916 /* JsonReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E590C7DB27E24894A7F5 /* JsonReader.swift */; }; 2475E73ECF60A8F034C74927 /* TestNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EBD6C8DCC2F5B14B1767 /* TestNavigator.swift */; }; + 2475E73F85667CAC7134D8F0 /* ExampleAccount.validate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E851F6A78217560BD5AD /* ExampleAccount.validate.swift */; }; 2475E74A49C8E47E8D24F59B /* RootViewControllerHolderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E42A6AA4FBBFB0E55EC0 /* RootViewControllerHolderStub.swift */; }; 2475E75FBBB9E38F12F79903 /* PagingCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EE8C681811D461007BA8 /* PagingCursorTests.swift */; }; 2475E77D353AD24284CB69B5 /* FilledLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E50C1059C69195EF3D11 /* FilledLayout.swift */; }; @@ -73,6 +75,7 @@ 2475E8169213EC2CBA0BA1B8 /* R.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E168B20E5C501400CF73 /* R.generated.swift */; }; 2475E82A2493936CC781D07D /* StargazersModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E9DFEA80E3745A0F2968 /* StargazersModelState.swift */; }; 2475E874F18C693DE83CE3DF /* StateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E9020284F55A4534822B /* StateMachine.swift */; }; + 2475E89759E20D18993926EF /* ValidationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E4941287EF47F70A30D6 /* ValidationResult.swift */; }; 2475E8F372F4DD9B057E7B9C /* PageRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E6E239D38735542EF44C /* PageRepositoryStub.swift */; }; 2475E8FA605B48473B57904A /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EB6B0F7A5FD41CBBED1D /* UserRepository.swift */; }; 2475E90D24B02A5C2FDA8D9D /* InfiniteScrollTrigger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E96088774A2C099B5023 /* InfiniteScrollTrigger.swift */; }; @@ -80,10 +83,14 @@ 2475E97E01676DBEA562211E /* EventSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E10518349B2428033335 /* EventSimulator.swift */; }; 2475E97F41F929B155A4C3E9 /* StargazersProgressViewBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E10213D6E811612BEB9A /* StargazersProgressViewBinding.swift */; }; 2475E9807E8143A4B3A751A5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2475E33610A7BBD163657C40 /* LaunchScreen.storyboard */; }; + 2475E98D7540F037083B638E /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475ED6AED412B75BD352F7A /* Validation.swift */; }; 2475E9CD20CEE8F26CCF1F4A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E2DD1B608720BA68982B /* AppDelegate.swift */; }; + 2475E9F0EFB736C7502B6861 /* ExampleAccountFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EB110AAC6A1C66853529 /* ExampleAccountFactory.swift */; }; + 2475EA0AECE4C6B9103E938F /* ExampleAccount.validateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475ED708C245A3863B99E1A /* ExampleAccount.validateTests.swift */; }; 2475EA187C6815D148DB8A4D /* ReverseNavigatorSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EC21DA75888B0FDD4C9F /* ReverseNavigatorSpy.swift */; }; 2475EA392C14FB9B3E28FADA /* UrlOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EFB7BA388A4179BF1988 /* UrlOpener.swift */; }; 2475EA90B85ECDDC24C29E54 /* PageRepositorySpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E04E146A951A851E9ED0 /* PageRepositorySpy.swift */; }; + 2475EA924F05407E64F4D789 /* ExampleValidationComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E5A696CBA9ACC3F75A8D /* ExampleValidationComposer.swift */; }; 2475EAAB190EE33192369B61 /* FontRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EA1B023ADAF16CC9FF85 /* FontRegistryTests.swift */; }; 2475EAC4F0BFB86680185B33 /* UrlOpenerSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E4BA9C3118702919AA54 /* UrlOpenerSpy.swift */; }; 2475EAC4F34D0000259F757C /* RemoteImageSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EA0712F2142C8DF66578 /* RemoteImageSourceTests.swift */; }; @@ -95,6 +102,7 @@ 2475EB8D267184D1BAB7F6E5 /* Bag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E7A8AA7111DAA6B2504A /* Bag.swift */; }; 2475EBB38F6CA1B500D1154E /* StargazersTableVIewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E998F32731441B1E1C21 /* StargazersTableVIewDataSource.swift */; }; 2475EBC38C427D41A452E006 /* GitHubStargazer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EA9AAFF3FDC512301D94 /* GitHubStargazer.swift */; }; + 2475EC39D3E04C52F05DCE59 /* ExampleAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E4EF943B6BAB7EB3F625 /* ExampleAccount.swift */; }; 2475EC486E18656C1908C298 /* TransparentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EC421B1F380C1E774ED4 /* TransparentViewController.swift */; }; 2475EC6A531495B33EEFC4B1 /* octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2475EFCEEABBB137955934D6 /* octicons.ttf */; }; 2475ECAEE1AC11CDD8108E02 /* BagStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E5837E0D2DB6DA54386E /* BagStub.swift */; }; @@ -103,18 +111,23 @@ 2475ED052F280DF12FCC21BF /* TestableDesignExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E52EE895096067C3AB7E /* TestableDesignExampleUITests.swift */; }; 2475ED056583F1ED24CD04C2 /* StargazersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EFFC22EBEE4556F83A10 /* StargazersModel.swift */; }; 2475ED0CFDCECF0B56005513 /* ModalDissolverStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E55F2CDD37D57C4B6BE7 /* ModalDissolverStub.swift */; }; + 2475ED25C1FFB5A6D29BFFAB /* ExampleValidationViewBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E026951DD5B3B3A3EF27 /* ExampleValidationViewBinding.swift */; }; 2475ED3436CA8F5D217CBC0E /* StargazerCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2475EE3031BDB39A34587324 /* StargazerCell.xib */; }; 2475ED3A098360D0EE41FBCF /* StargazersTableViewDataSourceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E7B61D5F345A77960ED3 /* StargazersTableViewDataSourceStub.swift */; }; 2475ED4D3251B46BE60ACE2D /* RootNavigatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E69F61196999EFB6AAAD /* RootNavigatorTests.swift */; }; 2475ED53BD3633732905F8AA /* StargazersModelStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EBF65AE582DF6BE0C031 /* StargazersModelStub.swift */; }; + 2475ED5502E5DBF809FC8124 /* ExampleValidationModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E34C2FD996FBF2799CFD /* ExampleValidationModelTests.swift */; }; 2475ED6B21CB5EEF515D37D8 /* GitHubApiEndpointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E6DFB3857B78D5216317 /* GitHubApiEndpointTests.swift */; }; 2475ED71E768A0D9244C7C8D /* StargazersMvcComposerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E53EEBB0B2F0A445AAC9 /* StargazersMvcComposerTests.swift */; }; 2475ED7AE6761CCDE02381AD /* StargazersRefreshController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E72ADB9B0178F9AEFF83 /* StargazersRefreshController.swift */; }; + 2475EDB7BAE07C8A8A1DB0C9 /* ExampleValidationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475EDBE2B453DCB2012F476 /* ExampleValidationModel.swift */; }; 2475EDC2FD509A53E43DEDA9 /* reposStargazers.json in Resources */ = {isa = PBXBuildFile; fileRef = 2475EFC5E1532F9230A85D82 /* reposStargazers.json */; }; 2475EDE03616EB9049E032A5 /* SpyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E10FA91065EBE65659A6 /* SpyViewController.swift */; }; 2475EE267AF728FF13F033B2 /* ModalPresenterStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E80441489D9BCA12BB8F /* ModalPresenterStub.swift */; }; 2475EE3247291D27A2E248FD /* GitHubUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E8EDF26D3584E8016653 /* GitHubUserStub.swift */; }; + 2475EE5A316B4958545E6AB1 /* ExampleValidationScreenRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E403C4E5292E7FCF4041 /* ExampleValidationScreenRootView.swift */; }; 2475EE8343FD232B5199749C /* R.generatedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E075557EBD6285CD11F6 /* R.generatedTests.swift */; }; + 2475EE8E54F8F840A42D69AD /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E4DFFAA24BF79779C06C /* Color.swift */; }; 2475EEC0350AE44691DDC0BA /* GitHubApiClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E0A01A4162606F18EA04 /* GitHubApiClientTests.swift */; }; 2475EEFB4FD740C3CAF18EF6 /* TestBedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E4C1270E87B3799F15BF /* TestBedViewController.swift */; }; 2475EF0B5F230503CEDCF81C /* GlobalModalPresenterStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E51655162C1CCBD1D9C6 /* GlobalModalPresenterStub.swift */; }; @@ -124,6 +137,7 @@ 2475EF62D7DC1E8FC2476A52 /* AsyncTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E30AD0FB5158B660EB33 /* AsyncTestHelper.swift */; }; 2475EF784651E52247E3C648 /* GitHubRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2475E9C17E4BBC136112F1C3 /* GitHubRepository.swift */; }; 2475EFE934412F4F12A7A9DF /* StargazersScreenRootView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2475E6D543F2FBEC69C94833 /* StargazersScreenRootView.xib */; }; + 627E9438215AC512000BDA62 /* ExampleValidationScreenRootView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 627E9437215AC512000BDA62 /* ExampleValidationScreenRootView.xib */; }; 629BBC831F90E262000BB6DA /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 629BBC821F90E262000BB6DA /* RxCocoa.framework */; }; 62A1614D1E73C1CC003D28DC /* RxBlocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62A1614B1E73C1CC003D28DC /* RxBlocking.framework */; }; 62A1614E1E73C1CC003D28DC /* RxTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62A1614C1E73C1CC003D28DC /* RxTest.framework */; }; @@ -154,6 +168,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 2475E026951DD5B3B3A3EF27 /* ExampleValidationViewBinding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleValidationViewBinding.swift; sourceTree = ""; }; 2475E04E146A951A851E9ED0 /* PageRepositorySpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageRepositorySpy.swift; sourceTree = ""; }; 2475E06647D6480FDBD2C6D6 /* VisualDecorator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualDecorator.swift; sourceTree = ""; }; 2475E075557EBD6285CD11F6 /* R.generatedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R.generatedTests.swift; sourceTree = ""; }; @@ -177,16 +192,21 @@ 2475E2DD1B608720BA68982B /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 2475E30AD0FB5158B660EB33 /* AsyncTestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncTestHelper.swift; sourceTree = ""; }; 2475E3444C1BA27DA348423B /* EventSimulator+UIScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EventSimulator+UIScrollView.swift"; sourceTree = ""; }; + 2475E34C2FD996FBF2799CFD /* ExampleValidationModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleValidationModelTests.swift; sourceTree = ""; }; 2475E39CF8EED23409B486F5 /* StargazersTableViewInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersTableViewInitializer.swift; sourceTree = ""; }; 2475E3BBDF2F0CD7483117FD /* UserScreenRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserScreenRootView.swift; sourceTree = ""; }; + 2475E403C4E5292E7FCF4041 /* ExampleValidationScreenRootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleValidationScreenRootView.swift; sourceTree = ""; }; 2475E40BE1B2CE78FAF70D47 /* UrlOpenerStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UrlOpenerStub.swift; sourceTree = ""; }; 2475E424E5349A6737062E54 /* InfiniteScrollTriggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfiniteScrollTriggerTests.swift; sourceTree = ""; }; 2475E42A6AA4FBBFB0E55EC0 /* RootViewControllerHolderStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewControllerHolderStub.swift; sourceTree = ""; }; 2475E44FF94942A600769656 /* UserRepositoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRepositoryTests.swift; sourceTree = ""; }; + 2475E4941287EF47F70A30D6 /* ValidationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidationResult.swift; sourceTree = ""; }; 2475E4A06E0651B68A798608 /* InfiniteScrollTriggerStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfiniteScrollTriggerStub.swift; sourceTree = ""; }; 2475E4A20AF63001833F7647 /* StargazersInfiniteScrollControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersInfiniteScrollControllerTests.swift; sourceTree = ""; }; 2475E4BA9C3118702919AA54 /* UrlOpenerSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UrlOpenerSpy.swift; sourceTree = ""; }; 2475E4C1270E87B3799F15BF /* TestBedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBedViewController.swift; sourceTree = ""; }; + 2475E4DFFAA24BF79779C06C /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; + 2475E4EF943B6BAB7EB3F625 /* ExampleAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleAccount.swift; sourceTree = ""; }; 2475E50C1059C69195EF3D11 /* FilledLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilledLayout.swift; sourceTree = ""; }; 2475E51655162C1CCBD1D9C6 /* GlobalModalPresenterStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalModalPresenterStub.swift; sourceTree = ""; }; 2475E52EE895096067C3AB7E /* TestableDesignExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableDesignExampleUITests.swift; sourceTree = ""; }; @@ -195,6 +215,7 @@ 2475E55F2CDD37D57C4B6BE7 /* ModalDissolverStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalDissolverStub.swift; sourceTree = ""; }; 2475E5837E0D2DB6DA54386E /* BagStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BagStub.swift; sourceTree = ""; }; 2475E590C7DB27E24894A7F5 /* JsonReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JsonReader.swift; sourceTree = ""; }; + 2475E5A696CBA9ACC3F75A8D /* ExampleValidationComposer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleValidationComposer.swift; sourceTree = ""; }; 2475E5B4D098FE6945B3F148 /* sample.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = sample.png; sourceTree = ""; }; 2475E5BE5363F823A04BC55E /* StargazersRefreshViewBinding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersRefreshViewBinding.swift; sourceTree = ""; }; 2475E62484C74FD0B910C6F5 /* StargazersRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersRepository.swift; sourceTree = ""; }; @@ -214,6 +235,7 @@ 2475E7DF6C438F3C12F13C5A /* PagingCursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingCursor.swift; sourceTree = ""; }; 2475E7E905777169DA3AAA02 /* AnyError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyError.swift; sourceTree = ""; }; 2475E80441489D9BCA12BB8F /* ModalPresenterStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalPresenterStub.swift; sourceTree = ""; }; + 2475E851F6A78217560BD5AD /* ExampleAccount.validate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleAccount.validate.swift; sourceTree = ""; }; 2475E855A890F447606F257C /* Navigator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Navigator.swift; sourceTree = ""; }; 2475E89DAFF095A125B23BCC /* StargazersRepositoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersRepositoryTests.swift; sourceTree = ""; }; 2475E8A65220DEDE700AE92A /* StargazersModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersModelTests.swift; sourceTree = ""; }; @@ -225,6 +247,7 @@ 2475E954DD31B11C6538425D /* EventSimulator+UIRefreshControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EventSimulator+UIRefreshControl.swift"; sourceTree = ""; }; 2475E96088774A2C099B5023 /* InfiniteScrollTrigger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfiniteScrollTrigger.swift; sourceTree = ""; }; 2475E96540D7FDFD907E38A1 /* GitHubUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubUser.swift; sourceTree = ""; }; + 2475E97FBFA43244E70A138D /* ExampleAccount.Draft.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleAccount.Draft.swift; sourceTree = ""; }; 2475E998F32731441B1E1C21 /* StargazersTableVIewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersTableVIewDataSource.swift; sourceTree = ""; }; 2475E9B6E1BD02A84A90344A /* ModalPresenterSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalPresenterSpy.swift; sourceTree = ""; }; 2475E9C17E4BBC136112F1C3 /* GitHubRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubRepository.swift; sourceTree = ""; }; @@ -240,6 +263,7 @@ 2475EA60787A5FB45051B288 /* ReverseNavigator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseNavigator.swift; sourceTree = ""; }; 2475EA9AAFF3FDC512301D94 /* GitHubStargazer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubStargazer.swift; sourceTree = ""; }; 2475EAAA03A6CC33D5660297 /* GitHubApiClientStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubApiClientStub.swift; sourceTree = ""; }; + 2475EB110AAC6A1C66853529 /* ExampleAccountFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleAccountFactory.swift; sourceTree = ""; }; 2475EB19DD3C94EAAF1EAEA4 /* UserModelStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserModelStub.swift; sourceTree = ""; }; 2475EB1E4113B6703D2B9BD0 /* StargazersNavigationViewBinding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersNavigationViewBinding.swift; sourceTree = ""; }; 2475EB3C1C05AA82BEB7602A /* GlobalModalPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalModalPresenterTests.swift; sourceTree = ""; }; @@ -261,8 +285,11 @@ 2475ED0DAFBC67961CB850A6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; 2475ED57920DD74087264FC7 /* TestableDesignExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestableDesignExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2475ED639ECF55577329F9D9 /* TestableDesignExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestableDesignExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2475ED6AED412B75BD352F7A /* Validation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Validation.swift; sourceTree = ""; }; + 2475ED708C245A3863B99E1A /* ExampleAccount.validateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleAccount.validateTests.swift; sourceTree = ""; }; 2475ED90991276AE1EA74B1D /* StargazersNavigationViewBindingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersNavigationViewBindingTests.swift; sourceTree = ""; }; 2475EDAB59CE5A5E99A03E61 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2475EDBE2B453DCB2012F476 /* ExampleValidationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleValidationModel.swift; sourceTree = ""; }; 2475EDEF357B21E401A94659 /* UserMvcComposerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserMvcComposerTests.swift; sourceTree = ""; }; 2475EE0C60E98CF0B0DD86DB /* RootViewControllerHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewControllerHolder.swift; sourceTree = ""; }; 2475EE3031BDB39A34587324 /* StargazerCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StargazerCell.xib; sourceTree = ""; }; @@ -277,6 +304,7 @@ 2475EFCEEABBB137955934D6 /* octicons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file.ttf; path = octicons.ttf; sourceTree = ""; }; 2475EFD023DE88AE709BCA17 /* StargazersRepositoryStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersRepositoryStub.swift; sourceTree = ""; }; 2475EFFC22EBEE4556F83A10 /* StargazersModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StargazersModel.swift; sourceTree = ""; }; + 627E9437215AC512000BDA62 /* ExampleValidationScreenRootView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ExampleValidationScreenRootView.xib; sourceTree = ""; }; 629BBC821F90E262000BB6DA /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/iOS/RxCocoa.framework; sourceTree = ""; }; 62A161471E73BCD4003D28DC /* Rswift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Rswift.framework; path = Carthage/Build/iOS/Rswift.framework; sourceTree = ""; }; 62A1614B1E73C1CC003D28DC /* RxBlocking.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxBlocking.framework; path = Carthage/Build/iOS/RxBlocking.framework; sourceTree = ""; }; @@ -352,6 +380,14 @@ path = View; sourceTree = ""; }; + 2475E0E56C495B6941240489 /* ViewBinding */ = { + isa = PBXGroup; + children = ( + 2475E026951DD5B3B3A3EF27 /* ExampleValidationViewBinding.swift */, + ); + path = ViewBinding; + sourceTree = ""; + }; 2475E12ABDB90E319AC6A747 /* UIKitSubClass */ = { isa = PBXGroup; children = ( @@ -433,6 +469,15 @@ path = StateMachine; sourceTree = ""; }; + 2475E3FC651E8756984C164D /* Validation */ = { + isa = PBXGroup; + children = ( + 2475E4941287EF47F70A30D6 /* ValidationResult.swift */, + 2475ED6AED412B75BD352F7A /* Validation.swift */, + ); + path = Validation; + sourceTree = ""; + }; 2475E45A8106355531E3A49E /* Shared */ = { isa = PBXGroup; children = ( @@ -446,6 +491,7 @@ 2475E46ADB4B81299769F17D /* VisualStyle */, 2475E1B9561BE67F13D3295E /* InfiniteScroll */, 2475E38D8C26C1B411E4A8F5 /* StateMachine */, + 2475E3FC651E8756984C164D /* Validation */, ); path = Shared; sourceTree = ""; @@ -467,6 +513,20 @@ path = Error; sourceTree = ""; }; + 2475E4E75210DB48371CA8CE /* Model */ = { + isa = PBXGroup; + children = ( + 2475E4EF943B6BAB7EB3F625 /* ExampleAccount.swift */, + 2475E97FBFA43244E70A138D /* ExampleAccount.Draft.swift */, + 2475E851F6A78217560BD5AD /* ExampleAccount.validate.swift */, + 2475ED708C245A3863B99E1A /* ExampleAccount.validateTests.swift */, + 2475EDBE2B453DCB2012F476 /* ExampleValidationModel.swift */, + 2475E34C2FD996FBF2799CFD /* ExampleValidationModelTests.swift */, + 2475EB110AAC6A1C66853529 /* ExampleAccountFactory.swift */, + ); + path = Model; + sourceTree = ""; + }; 2475E54EC10A2464951D9E0E /* GitHub */ = { isa = PBXGroup; children = ( @@ -505,6 +565,7 @@ 2475E45A8106355531E3A49E /* Shared */, 2475E03D39E9FDB714DC39B2 /* Stargazers */, 2475EA25575CDC3E6312B17A /* User */, + 2475EE40C10180060A4EC8CF /* Validation */, ); path = MvcArchitecture; sourceTree = ""; @@ -567,6 +628,15 @@ path = ViewBinding; sourceTree = ""; }; + 2475E90AEF5B29774D19C929 /* UIKitSubClass */ = { + isa = PBXGroup; + children = ( + 2475E403C4E5292E7FCF4041 /* ExampleValidationScreenRootView.swift */, + 627E9437215AC512000BDA62 /* ExampleValidationScreenRootView.xib */, + ); + path = UIKitSubClass; + sourceTree = ""; + }; 2475E927630C40FFEC2AE23B /* TestableDesignExampleTests */ = { isa = PBXGroup; children = ( @@ -726,6 +796,16 @@ path = UITableView; sourceTree = ""; }; + 2475EE40C10180060A4EC8CF /* Validation */ = { + isa = PBXGroup; + children = ( + 2475E5A696CBA9ACC3F75A8D /* ExampleValidationComposer.swift */, + 2475E4E75210DB48371CA8CE /* Model */, + 2475EEBCC2C7AC3AED1E83F1 /* View */, + ); + path = Validation; + sourceTree = ""; + }; 2475EE45BEDD0230515711A7 /* Bootstrap */ = { isa = PBXGroup; children = ( @@ -734,6 +814,15 @@ path = Bootstrap; sourceTree = ""; }; + 2475EEBCC2C7AC3AED1E83F1 /* View */ = { + isa = PBXGroup; + children = ( + 2475E0E56C495B6941240489 /* ViewBinding */, + 2475E90AEF5B29774D19C929 /* UIKitSubClass */, + ); + path = View; + sourceTree = ""; + }; 2475EEFA8615701D3EDF6A2D /* UIView */ = { isa = PBXGroup; children = ( @@ -832,6 +921,7 @@ children = ( 2475EF9000BC9B4E5171177D /* Font */, 2475EA15DFBD6DF9C26116F6 /* R.swift */, + 2475E4DFFAA24BF79779C06C /* Color.swift */, ); path = Resources; sourceTree = ""; @@ -937,6 +1027,7 @@ 2475E184C491ECB5D4520C4D /* Assets.xcassets in Resources */, 2475E9807E8143A4B3A751A5 /* LaunchScreen.storyboard in Resources */, 2475ED3436CA8F5D217CBC0E /* StargazerCell.xib in Resources */, + 627E9438215AC512000BDA62 /* ExampleValidationScreenRootView.xib in Resources */, 2475EFE934412F4F12A7A9DF /* StargazersScreenRootView.xib in Resources */, 2475E1A99BDA436628DAEAB6 /* UserScreenRootView.xib in Resources */, 2475EC6A531495B33EEFC4B1 /* octicons.ttf in Resources */, @@ -993,7 +1084,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$SRCROOT/libexec/rswift\" \"$SRCROOT/TestableDesignExample/Resources/R.swift/\""; + shellScript = "\"$SRCROOT/libexec/rswift\" \"$SRCROOT/TestableDesignExample/Resources/R.swift/\"\n"; }; 62A1614F1E73C1D2003D28DC /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1022,7 +1113,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$SRCROOT/libexec/rswift\" \"$SRCROOT/TestableDesignExampleTests/Resources/\""; + shellScript = "\"$SRCROOT/libexec/rswift\" \"$SRCROOT/TestableDesignExampleTests/Resources/\"\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1091,6 +1182,16 @@ 2475E874F18C693DE83CE3DF /* StateMachine.swift in Sources */, 2475E8FA605B48473B57904A /* UserRepository.swift in Sources */, 2475E1A9256AB42E87A318F1 /* StargazersTableViewInitializer.swift in Sources */, + 2475EA924F05407E64F4D789 /* ExampleValidationComposer.swift in Sources */, + 2475EC39D3E04C52F05DCE59 /* ExampleAccount.swift in Sources */, + 2475E485404E14B466A00D45 /* ExampleAccount.Draft.swift in Sources */, + 2475E73F85667CAC7134D8F0 /* ExampleAccount.validate.swift in Sources */, + 2475E89759E20D18993926EF /* ValidationResult.swift in Sources */, + 2475E98D7540F037083B638E /* Validation.swift in Sources */, + 2475EDB7BAE07C8A8A1DB0C9 /* ExampleValidationModel.swift in Sources */, + 2475EE5A316B4958545E6AB1 /* ExampleValidationScreenRootView.swift in Sources */, + 2475ED25C1FFB5A6D29BFFAB /* ExampleValidationViewBinding.swift in Sources */, + 2475EE8E54F8F840A42D69AD /* Color.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1154,6 +1255,9 @@ 2475EE3247291D27A2E248FD /* GitHubUserStub.swift in Sources */, 2475E4258308ED2D3361DB75 /* UserRepositoryTests.swift in Sources */, 2475E241A349C78369EB1CA4 /* StargazersRepositoryTests.swift in Sources */, + 2475EA0AECE4C6B9103E938F /* ExampleAccount.validateTests.swift in Sources */, + 2475ED5502E5DBF809FC8124 /* ExampleValidationModelTests.swift in Sources */, + 2475E9F0EFB736C7502B6861 /* ExampleAccountFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TestableDesignExample.xcodeproj/xcshareddata/xcschemes/TestableDesignExampleTests.xcscheme b/TestableDesignExample.xcodeproj/xcshareddata/xcschemes/TestableDesignExampleTests.xcscheme new file mode 100644 index 0000000..e27665c --- /dev/null +++ b/TestableDesignExample.xcodeproj/xcshareddata/xcschemes/TestableDesignExampleTests.xcscheme @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestableDesignExample/MvcArchitecture/Shared/Validation/Validation.swift b/TestableDesignExample/MvcArchitecture/Shared/Validation/Validation.swift new file mode 100644 index 0000000..2705e62 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Shared/Validation/Validation.swift @@ -0,0 +1,32 @@ +import Foundation + + +// NOTE: In general, CharacterSet is not equivalent to Set, but equivalent to Set. +// SEE: https://github.com/apple/swift/blob/swift-4.0-RELEASE/docs/StringManifesto.md#character-and-characterset +let asciiLowerAlpha = Set("abcdefghijklmnopqrstuvwxyz") +let asciiUpperAlpha = Set("ABCDEFGHIJKLMNOPQRSTUVWXYZ") +let asciiAlpha = asciiLowerAlpha.union(asciiUpperAlpha) +let asciiDigit = Set("0123456789") +let asciiAlphaNumeric = asciiAlpha.union(asciiDigit) +let asciiSymbol = Set(" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") +let asciiPrintable = asciiAlphaNumeric.union(asciiSymbol) + + + +func characters(in text: String, without characters: Set) -> Set { + var characterSet = Set(text) + characterSet.subtract(characters) + return characterSet +} + + + +func string(from characters: Set) -> String { + var result = "" + + characters.sorted().forEach { character in + result.append(character) + } + + return result +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Shared/Validation/ValidationResult.swift b/TestableDesignExample/MvcArchitecture/Shared/Validation/ValidationResult.swift new file mode 100644 index 0000000..265b48c --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Shared/Validation/ValidationResult.swift @@ -0,0 +1,49 @@ +enum ValidationResult { + case success(V) + case failure(because: E) + + + var isSuccess: Bool { + switch self { + case .success: + return true + case .failure: + return false + } + } + + + var value: V? { + switch self { + case .success(let value): + return value + case .failure: + return nil + } + } + + + var reason: E? { + switch self { + case .success: + return nil + case .failure(because: let reason): + return reason + } + } +} + + + +extension ValidationResult: Equatable where V: Equatable, E: Equatable { + static func ==(lhs: ValidationResult, rhs: ValidationResult) -> Bool { + switch (lhs, rhs) { + case (.success(let l), .success(let r)): + return l == r + case (.failure(because: let l), .failure(because: let r)): + return l == r + default: + return false + } + } +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/ExampleValidationComposer.swift b/TestableDesignExample/MvcArchitecture/Validation/ExampleValidationComposer.swift new file mode 100644 index 0000000..d642dca --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/ExampleValidationComposer.swift @@ -0,0 +1,6 @@ +import UIKit + + +class ExampleValidationComposer: UIViewController { + +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.Draft.swift b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.Draft.swift new file mode 100644 index 0000000..8ad05f2 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.Draft.swift @@ -0,0 +1,11 @@ +extension ExampleAccount { + struct Draft { + let userName: String + let password: String + + + static func createEmpty() -> Draft { + return Draft(userName: "", password: "") + } + } +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.swift b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.swift new file mode 100644 index 0000000..3c0c666 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.swift @@ -0,0 +1,14 @@ +struct ExampleAccount: Equatable { + let userName: UserName + let password: Password + + + struct UserName: Equatable { + let text: String + } + + + struct Password: Equatable { + let text: String + } +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validate.swift b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validate.swift new file mode 100644 index 0000000..05644f4 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validate.swift @@ -0,0 +1,156 @@ +import Foundation + + + +extension ExampleAccount.Draft { + static func validate(draft: ExampleAccount.Draft) -> ValidationResult { + let userNameResult = ExampleAccount.UserName.validate(userName: draft.userName) + let passwordResult = ExampleAccount.Password.validate(password: draft.password, userName: draft.userName) + + switch (userNameResult, passwordResult) { + case (.success(let userName), .success(let password)): + return .success(ExampleAccount( + userName: userName, + password: password + )) + + case (.failure(because: let userNameReason), .success): + return .failure(because: InvalidReason( + userName: userNameReason, + password: [] + )) + + case (.success, .failure(because: let passwordReason)): + return .failure(because: InvalidReason( + userName: [], + password: passwordReason + )) + + case (.failure(because: let userNameReason), .failure(because: let passwordReason)): + return .failure(because: InvalidReason( + userName: userNameReason, + password: passwordReason + )) + } + } + + + struct InvalidReason: Hashable { + let userName: Set + let password: Set + } +} + + +extension ExampleAccount.UserName { + private static let acceptableCharacters = CharacterSet.letters + + + static func validate(userName: String) -> ValidationResult> { + var reasons = Set() + + if userName.count < 4 { + reasons.insert(.shorterThan4) + } + + if userName.count > 30 { + reasons.insert(.longerThan30) + } + + let invalidChars = characters(in: userName, without: asciiAlphaNumeric) + if !invalidChars.isEmpty { + reasons.insert(.hasUnavailableChars(found: invalidChars)) + } + + guard reasons.isEmpty else { + return .failure(because: reasons) + } + + return .success(ExampleAccount.UserName(text: userName)) + } + + + enum InvalidReason: Hashable, Comparable { + case shorterThan4 + case longerThan30 + case hasUnavailableChars(found: Set) + + + static func <(lhs: InvalidReason, rhs: InvalidReason) -> Bool { + switch (lhs, rhs) { + case (_, .shorterThan4): + return false + case (.shorterThan4, _): + return true + case (_, .longerThan30): + return false + case (.longerThan30, _): + return true + case (_, .hasUnavailableChars): + return false + case (.hasUnavailableChars, _): + return true + } + } + } +} + + +extension ExampleAccount.Password { + static func validate(password: String, userName: String) -> ValidationResult> { + var reasons = Set() + + if password.count < 8 { + reasons.insert(.shorterThan8) + } + + if password.count > 100 { + reasons.insert(.longerThan100) + } + + if password == userName { + reasons.insert(.sameAsUserName) + } + + let invalidChars = characters(in: password, without: asciiPrintable) + if !invalidChars.isEmpty { + reasons.insert(.hasUnavailableChars(found: invalidChars)) + } + + guard reasons.isEmpty else { + return .failure(because: reasons) + } + + return .success(ExampleAccount.Password(text: password)) + } + + + enum InvalidReason: Hashable, Comparable { + case shorterThan8 + case longerThan100 + case hasUnavailableChars(found: Set) + case sameAsUserName + + + static func <(lhs: InvalidReason, rhs: InvalidReason) -> Bool { + switch (lhs, rhs) { + case (_, .shorterThan8): + return false + case (.shorterThan8, _): + return true + case (_, .longerThan100): + return false + case (.longerThan100, _): + return true + case (_, .hasUnavailableChars): + return false + case (.hasUnavailableChars, _): + return true + case (_, .sameAsUserName): + return false + case (.sameAsUserName, _): + return true + } + } + } +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validateTests.swift b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validateTests.swift new file mode 100644 index 0000000..60776e6 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccount.validateTests.swift @@ -0,0 +1,192 @@ +import XCTest +import MirrorDiffKit +@testable import TestableDesignExample + + + +class ExampleAccountTests: XCTestCase { + func testValidate() { + typealias TestCase = ( + input: ExampleAccount.Draft, + expected: ValidationResult + ) + + let testCases: [UInt: TestCase] = [ + #line: ( + input: .init( + userName: "userName", + password: "password" + ), + expected: .success(ExampleAccount( + userName: .init(text: "userName"), + password: .init(text: "password") + )) + ), + #line: ( + input: .init( + userName: "", + password: "password" + ), + expected: .failure( + because: .init(userName: [.shorterThan4], password: []) + ) + ), + #line: ( + input: .init( + userName: "userName", + password: "" + ), + expected: .failure( + because: .init(userName: [], password: [.shorterThan8]) + ) + ), + #line: ( + input: .init( + userName: "u", + password: "p" + ), + expected: .failure( + because: .init( + userName: [.shorterThan4], + password: [.shorterThan8] + ) + ) + ), + #line: ( + input: .init( + userName: "userName", + password: "userName" + ), + expected: .failure( + because: .init( + userName: [], + password: [.sameAsUserName] + ) + ) + ), + ] + + testCases.forEach { tuple in + let (line, (input: draft, expected: expected)) = tuple + + let actual = ExampleAccount.Draft.validate(draft: draft) + + XCTAssertEqual(expected, actual, diff(between: actual, and: expected), line: line) + } + } +} + + + +class ExampleAccountUserNameTests: XCTestCase { + func testValidate() { + typealias TestCase = ( + input: String, + expected: ValidationResult> + ) + + let testCases: [UInt: TestCase] = [ + #line: ( + input: "", + expected: .failure(because: [.shorterThan4]) + ), + #line: ( + input: String(repeating: "x", count: 3), + expected: .failure(because: [.shorterThan4]) + ), + #line: ( + input: String(repeating: "x", count: 4), + expected: .success(.init(text: String(repeating: "x", count: 4))) + ), + #line: ( + input: String(repeating: "x", count: 30), + expected: .success(.init(text: String(repeating: "x", count: 30))) + ), + #line: ( + input: String(repeating: "x", count: 31), + expected: .failure(because: [.longerThan30]) + ), + #line: ( + input: string(from: asciiDigit), + expected: .success(.init(text: string(from: asciiDigit))) + ), + #line: ( + input: string(from: asciiLowerAlpha), + expected: .success(.init(text: string(from: asciiLowerAlpha))) + ), + #line: ( + input: string(from: asciiUpperAlpha), + expected: .success(.init(text: string(from: asciiUpperAlpha))) + ), + #line: ( + input: "abcd1234ABCD", + expected: .success(.init(text: "abcd1234ABCD")) + ), + #line: ( + input: string(from: asciiSymbol), + expected: .failure(because: [ + .longerThan30, + .hasUnavailableChars(found: asciiSymbol), + ]) + ), + ] + + testCases.forEach { tuple in + let (line, (input: input, expected: expected)) = tuple + + let actual = ExampleAccount.UserName.validate(userName: input) + + XCTAssertEqual(expected, actual, diff(between: actual, and: expected), line: line) + } + } +} + + + +class ExampleAccountPasswordTests: XCTestCase { + func testValidate() { + typealias TestCase = ( + input: (password: String, userName: String), + expected: ValidationResult> + ) + + let testCases: [UInt: TestCase] = [ + #line: ( + input: (password: "", userName: "userName"), + expected: .failure(because: [.shorterThan8]) + ), + #line: ( + input: (password: String(repeating: "x", count: 7), userName: "userName"), + expected: .failure(because: [.shorterThan8]) + ), + #line: ( + input: (password: String(repeating: "x", count: 8), userName: "userName"), + expected: .success(.init(text: String(repeating: "x", count: 8))) + ), + #line: ( + input: (password: String(repeating: "x", count: 100), userName: "userName"), + expected: .success(.init(text: String(repeating: "x", count: 100))) + ), + #line: ( + input: (password: String(repeating: "x", count: 101), userName: "userName"), + expected: .failure(because: [.longerThan100]) + ), + #line: ( + input: (password: string(from: asciiPrintable), userName: "userName"), + expected: .success(.init(text: string(from: asciiPrintable))) + ), + #line: ( + input: (password: "userName", userName: "userName"), + expected: .failure(because: [.sameAsUserName]) + ), + ] + + testCases.forEach { tuple in + let (line, (input: (password: password, userName: userName), expected: expected)) = tuple + + let actual = ExampleAccount.Password.validate(password: password, userName: userName) + + XCTAssertEqual(expected, actual, diff(between: actual, and: expected), line: line) + } + } +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccountFactory.swift b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccountFactory.swift new file mode 100644 index 0000000..d255cf7 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleAccountFactory.swift @@ -0,0 +1,33 @@ +@testable import TestableDesignExample + + + +enum ExampleAccountFactory { + static func create( + userName: ExampleAccount.UserName = UserNameFactory.create(), + password: ExampleAccount.Password = PasswordFactory.create() + ) -> ExampleAccount { + return ExampleAccount(userName: userName, password: password) + } + + + enum UserNameFactory { + static func create(text: String = "userName") -> ExampleAccount.UserName { + return .init(text: text) + } + } + + + enum PasswordFactory { + static func create(text: String = "userName") -> ExampleAccount.Password { + return .init(text: text) + } + } + + + enum DraftFactory { + static func create(userName: String = "", password: String = "") -> ExampleAccount.Draft { + return ExampleAccount.Draft(userName: userName, password: password) + } + } +} diff --git a/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModel.swift b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModel.swift new file mode 100644 index 0000000..011d323 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModel.swift @@ -0,0 +1,49 @@ +import RxCocoa + + + +protocol ExampleValidationModelProtocol { + var currentState: ExampleValidationModelState { get } + var didChange: RxCocoa.Driver { get } + + func update(by draft: ExampleAccount.Draft) +} + + + +enum ExampleValidationModelState: Equatable { + case notValidatedYet + case validated(ValidationResult) +} + + + +class ExampleValidationModel: ExampleValidationModelProtocol { + typealias Strategy = (ExampleAccount.Draft) -> ValidationResult + + + private let stateMachine: StateMachine + private let validate: Strategy + + + var currentState: ExampleValidationModelState { + return self.stateMachine.currentState + } + + + var didChange: Driver { + return self.stateMachine.didChange + } + + + init(startingWith initialState: ExampleValidationModelState, validatingBy strategy: @escaping Strategy) { + self.stateMachine = StateMachine(startingWith: initialState) + self.validate = strategy + } + + + func update(by draft: ExampleAccount.Draft) { + let result = self.validate(draft) + self.stateMachine.transit(to: .validated(result)) + } +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModelTests.swift b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModelTests.swift new file mode 100644 index 0000000..fbb181d --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/Model/ExampleValidationModelTests.swift @@ -0,0 +1,80 @@ +import XCTest +import MirrorDiffKit +@testable import TestableDesignExample + + + +class ExampleValidationModelTests: XCTestCase { + func testNotValidatedYet() { + let model = ExampleValidationModel( + startingWith: .notValidatedYet, + validatingBy: self.createDummyStrategy() + ) + + let actual = model.currentState + + let expected: ExampleValidationModelState = .notValidatedYet + XCTAssertEqual(actual, expected, diff(between: expected, and: actual)) + } + + + func testSuccess() { + let account = ExampleAccountFactory.create() + let model = ExampleValidationModel( + startingWith: .notValidatedYet, + validatingBy: self.createSuccessfulStrategy(account: account) + ) + + let anyDraft = ExampleAccountFactory.DraftFactory.create() + model.update(by: anyDraft) + + let actual = model.currentState + let expected: ExampleValidationModelState = .validated(.success(account)) + XCTAssertEqual(actual, expected, diff(between: expected, and: actual)) + } + + + func testFailure() { + let reason = self.createAnyDraftInvalidReasonSet() + let model = ExampleValidationModel( + startingWith: .notValidatedYet, + validatingBy: self.createFailedStrategy(reason: reason) + ) + + let anyDraft = ExampleAccountFactory.DraftFactory.create() + model.update(by: anyDraft) + + let actual = model.currentState + let expected: ExampleValidationModelState = .validated(.failure(because: reason)) + XCTAssertEqual(actual, expected, diff(between: expected, and: actual)) + } + + + private func createDummyStrategy() -> ExampleValidationModel.Strategy { + return { _ in + fatalError("It should not affect to test results") + } + } + + + private func createSuccessfulStrategy(account: ExampleAccount) -> ExampleValidationModel.Strategy { + return { _ in + return .success(account) + } + } + + + private func createFailedStrategy(reason: ExampleAccount.Draft.InvalidReason) -> ExampleValidationModel.Strategy { + return { _ in + return .failure(because: reason) + } + } + + + private func createAnyDraftInvalidReasonSet() -> ExampleAccount.Draft.InvalidReason { + return ExampleAccount.Draft.InvalidReason( + userName: [.shorterThan4], + password: [.shorterThan8] + ) + } +} \ No newline at end of file diff --git a/TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.swift b/TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.swift new file mode 100644 index 0000000..dee6032 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.swift @@ -0,0 +1,35 @@ +import UIKit + + + +class ExampleValidationScreenRootView: UIView { + @IBOutlet weak var nameTextField: UITextField! + @IBOutlet weak var passwordTextField: UITextField! + @IBOutlet weak var userNameHintLabel: UILabel! + @IBOutlet weak var passwordHintLabel: UILabel! + + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadFromXib() + } + + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadFromXib() + } + + + private func loadFromXib() { + guard let view = R.nib.exampleValidationScreenRootView.firstView(owner: self) else { + return + } + + view.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(view) + + FilledLayout.fill(subview: view, into: self) + self.layoutIfNeeded() + } +} diff --git a/TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.xib b/TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.xib new file mode 100644 index 0000000..d138849 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/View/UIKitSubClass/ExampleValidationScreenRootView.xib @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestableDesignExample/MvcArchitecture/Validation/View/ViewBinding/ExampleValidationViewBinding.swift b/TestableDesignExample/MvcArchitecture/Validation/View/ViewBinding/ExampleValidationViewBinding.swift new file mode 100644 index 0000000..402a596 --- /dev/null +++ b/TestableDesignExample/MvcArchitecture/Validation/View/ViewBinding/ExampleValidationViewBinding.swift @@ -0,0 +1,83 @@ +import UIKit +import RxSwift +import RxCocoa + + + +protocol ExampleValidationViewBindingProtocol {} + + + +class ExampleValidationViewBinding: ExampleValidationViewBindingProtocol { + private let disposeBag = RxSwift.DisposeBag() + private let model: ExampleValidationModelProtocol + private let view: ExampleValidationScreenRootView + + + init( + observing model: ExampleValidationModelProtocol, + handling view: ExampleValidationScreenRootView + ) { + self.model = model + self.view = view + + self.model.didChange + .drive(onNext: { [weak self] state in + guard let this = self else { return } + + switch state { + case .notValidatedYet: + this.view.userNameHintLabel.text = "" + this.view.userNameHintLabel.backgroundColor = ColorPalette.Form.Background.normal + this.view.passwordHintLabel.text = "" + this.view.passwordHintLabel.backgroundColor = ColorPalette.Form.Background.normal + + case .validated(.success): + this.view.userNameHintLabel.text = "" + this.view.userNameHintLabel.backgroundColor = ColorPalette.Form.Background.ok + this.view.passwordHintLabel.text = "" + this.view.passwordHintLabel.backgroundColor = ColorPalette.Form.Background.ok + + case .validated(.failure(because: let reason)): + if let userNameReason = reason.userName.sorted().first { + let userNameHint: String + switch userNameReason { + case .shorterThan4: + userNameHint = "Must be longer than 8" + case .longerThan30: + userNameHint = "Must be shorter than 100" + case .hasUnavailableChars(found: let characters): + userNameHint = "Unavailable characters: \(string(from: characters))" + } + this.view.userNameHintLabel.text = userNameHint + this.view.userNameHintLabel.backgroundColor = ColorPalette.Form.Background.ng + } + else { + this.view.userNameHintLabel.text = "" + this.view.userNameHintLabel.backgroundColor = ColorPalette.Form.Background.ok + } + + if let passwordReason = reason.password.sorted().first { + let passwordHint: String + switch passwordReason { + case .shorterThan8: + passwordHint = "Must be longer than 8" + case .longerThan100: + passwordHint = "Must be shorter than 100" + case .hasUnavailableChars(found: let characters): + passwordHint = "Unavailable characters: \(string(from: characters))" + case .sameAsUserName: + passwordHint = "Must be difference the user name" + } + this.view.passwordHintLabel.text = passwordHint + this.view.passwordHintLabel.backgroundColor = ColorPalette.Form.Background.ng + } + else { + this.view.passwordHintLabel.text = "" + this.view.passwordHintLabel.backgroundColor = ColorPalette.Form.Background.ok + } + } + }) + .disposed(by: self.disposeBag) + } +} \ No newline at end of file diff --git a/TestableDesignExample/Resources/Color.swift b/TestableDesignExample/Resources/Color.swift new file mode 100644 index 0000000..7c3c480 --- /dev/null +++ b/TestableDesignExample/Resources/Color.swift @@ -0,0 +1,32 @@ +import UIKit + + + +enum ColorPalette { + enum Form { + enum Background { + static let normal = UIColor( + hue: 0.666, + saturation: 0.020, + brightness: 0.960, + alpha: 0 + ) + + + static let ng = UIColor( + hue: 0.005, + saturation: 0.280, + brightness: 1, + alpha: 1 + ) + + + static let ok = UIColor( + hue: 0.311, + saturation: 0.280, + brightness: 1, + alpha: 1 + ) + } + } +} \ No newline at end of file