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/Cartfile.resolved b/Cartfile.resolved
index e4cae3e..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" "f043778214c8f182018ccdfbf7f440edbe0aecc8"
+github "ReactiveX/RxSwift" "0df62b4d562f8620d4b795b18e4adf0b631527a1"
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..0df62b4 160000
--- a/Carthage/Checkouts/RxSwift
+++ b/Carthage/Checkouts/RxSwift
@@ -1 +1 @@
-Subproject commit f043778214c8f182018ccdfbf7f440edbe0aecc8
+Subproject commit 0df62b4d562f8620d4b795b18e4adf0b631527a1
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/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
+
+
+
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