├── .gitignore ├── .swiftlint.yml ├── Podfile ├── Podfile.lock ├── TodoList.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── gyuwon.xcuserdatad │ └── xcschemes │ ├── TodoList.xcscheme │ └── xcschememanagement.plist ├── TodoList.xcworkspace └── contents.xcworkspacedata ├── TodoList ├── AppDelegate.swift ├── ApplicationModel.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── MessageBox.swift ├── NewTodoItemFormViewController.swift ├── NewTodoItemFormViewModel.swift ├── ObservableCollection.swift ├── RelayCommand.swift ├── ServiceLocator.swift ├── TodoItemListViewController.swift ├── TodoItemListViewModel.swift ├── TodoItemViewModel.swift └── ViewModelLocator.swift ├── TodoListTests ├── Info.plist ├── NewTodoItemFormViewModel_specs.swift ├── ObservableCollection_specs.swift ├── RelayCommand_specs.swift ├── StubMessageBox.swift ├── TodoItemListViewModel_features.swift └── TodoItemViewModel_specs.swift └── TodoListUITests ├── Info.plist └── TodoListUITests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | DerivedData/ 3 | 4 | xcuserdata/ 5 | 6 | Pods/ 7 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - TodoList 3 | - TodoListTests 4 | - TodoListUITests 5 | excluded: 6 | - Pods 7 | - DerivedData 8 | disabled_rules: 9 | - line_length 10 | - trailing_whitespace 11 | - trailing_newline 12 | - type_name 13 | - function_parameter_count 14 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'TodoList' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for TodoList 9 | pod 'RxSwift', '~> 3.0' 10 | pod 'RxCocoa', '~> 3.0' 11 | pod 'SwiftLint' 12 | 13 | target 'TodoListTests' do 14 | inherit! :search_paths 15 | # Pods for testing 16 | end 17 | 18 | target 'TodoListUITests' do 19 | inherit! :search_paths 20 | # Pods for testing 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RxCocoa (3.2.0): 3 | - RxSwift (~> 3.1) 4 | - RxSwift (3.2.0) 5 | - SwiftLint (0.16.1) 6 | 7 | DEPENDENCIES: 8 | - RxCocoa (~> 3.0) 9 | - RxSwift (~> 3.0) 10 | - SwiftLint 11 | 12 | SPEC CHECKSUMS: 13 | RxCocoa: ccdf43101a70407097a29082f648ba1676075b30 14 | RxSwift: 46574f70d416b7923c237195939cc488a7fbf3a0 15 | SwiftLint: b8b683208cc09640898f16318a7a452274e91f61 16 | 17 | PODFILE CHECKSUM: bad54c3f8db8d13cadb5ca6193a1913c68b66e88 18 | 19 | COCOAPODS: 1.2.0 20 | -------------------------------------------------------------------------------- /TodoList.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4D120B8E221C84C0DD5895A5 /* Pods_TodoList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1EB14034D3DCB48191FEABD /* Pods_TodoList.framework */; }; 11 | A41302F31E7D9DC100B59EE8 /* TodoItemListViewModel_features.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41302F21E7D9DC100B59EE8 /* TodoItemListViewModel_features.swift */; }; 12 | A41302F61E7DABC600B59EE8 /* MessageBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41302F51E7DABC600B59EE8 /* MessageBox.swift */; }; 13 | A41302F81E7DAE1500B59EE8 /* ServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41302F71E7DAE1500B59EE8 /* ServiceLocator.swift */; }; 14 | A41302FA1E7DB3A800B59EE8 /* StubMessageBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41302F91E7DB3A800B59EE8 /* StubMessageBox.swift */; }; 15 | A44B28F21E750A4B0068BCB8 /* TodoItemListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B28F11E750A4B0068BCB8 /* TodoItemListViewModel.swift */; }; 16 | A44B28F41E750BD20068BCB8 /* NewTodoItemFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B28F31E750BD20068BCB8 /* NewTodoItemFormViewModel.swift */; }; 17 | A44B28F61E750D3A0068BCB8 /* ApplicationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B28F51E750D3A0068BCB8 /* ApplicationModel.swift */; }; 18 | A44B28FA1E7514670068BCB8 /* NewTodoItemFormViewModel_specs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B28F91E7514670068BCB8 /* NewTodoItemFormViewModel_specs.swift */; }; 19 | A44B28FC1E7517180068BCB8 /* RelayCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B28FB1E7517180068BCB8 /* RelayCommand.swift */; }; 20 | A44B28FE1E751A2B0068BCB8 /* RelayCommand_specs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B28FD1E751A2B0068BCB8 /* RelayCommand_specs.swift */; }; 21 | A44E807E1E744A8D00E41C77 /* TodoItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44E807D1E744A8D00E41C77 /* TodoItemViewModel.swift */; }; 22 | A44E80801E7472A900E41C77 /* ObservableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44E807F1E7472A900E41C77 /* ObservableCollection.swift */; }; 23 | A44E80821E74F8CE00E41C77 /* ObservableCollection_specs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44E80811E74F8CE00E41C77 /* ObservableCollection_specs.swift */; }; 24 | A49242C81E75513800BBEA4E /* ViewModelLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49242C71E75513800BBEA4E /* ViewModelLocator.swift */; }; 25 | A4B90BDB1E754AAD00297ED2 /* TodoItemListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B90BDA1E754AAD00297ED2 /* TodoItemListViewController.swift */; }; 26 | A4C5033E1E741BA200E1F149 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C5033D1E741BA200E1F149 /* AppDelegate.swift */; }; 27 | A4C503451E741BA200E1F149 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A4C503431E741BA200E1F149 /* Main.storyboard */; }; 28 | A4C503471E741BA200E1F149 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A4C503461E741BA200E1F149 /* Assets.xcassets */; }; 29 | A4C5034A1E741BA200E1F149 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A4C503481E741BA200E1F149 /* LaunchScreen.storyboard */; }; 30 | A4C503551E741BA200E1F149 /* TodoItemViewModel_specs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C503541E741BA200E1F149 /* TodoItemViewModel_specs.swift */; }; 31 | A4C503601E741BA200E1F149 /* TodoListUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C5035F1E741BA200E1F149 /* TodoListUITests.swift */; }; 32 | A4FCD0E81E7552F700D14BBD /* NewTodoItemFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FCD0E71E7552F700D14BBD /* NewTodoItemFormViewController.swift */; }; 33 | C1A79D4A86CB57BE764BD8C5 /* Pods_TodoListTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4DC58928149C92522442E0F /* Pods_TodoListTests.framework */; }; 34 | D170125F063A0BF80174D16F /* Pods_TodoListUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24914B092EA65AADC9E21E9F /* Pods_TodoListUITests.framework */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXContainerItemProxy section */ 38 | A4C503511E741BA200E1F149 /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = A4C503321E741BA200E1F149 /* Project object */; 41 | proxyType = 1; 42 | remoteGlobalIDString = A4C503391E741BA200E1F149; 43 | remoteInfo = TodoList; 44 | }; 45 | A4C5035C1E741BA200E1F149 /* PBXContainerItemProxy */ = { 46 | isa = PBXContainerItemProxy; 47 | containerPortal = A4C503321E741BA200E1F149 /* Project object */; 48 | proxyType = 1; 49 | remoteGlobalIDString = A4C503391E741BA200E1F149; 50 | remoteInfo = TodoList; 51 | }; 52 | /* End PBXContainerItemProxy section */ 53 | 54 | /* Begin PBXFileReference section */ 55 | 1F05CF88C2D4591B3EA080E1 /* Pods-TodoListUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodoListUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-TodoListUITests/Pods-TodoListUITests.release.xcconfig"; sourceTree = ""; }; 56 | 24914B092EA65AADC9E21E9F /* Pods_TodoListUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TodoListUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 2D797FAE6A9956E1441B2798 /* Pods-TodoListTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodoListTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-TodoListTests/Pods-TodoListTests.release.xcconfig"; sourceTree = ""; }; 58 | 2FEF863441818AD5AA859010 /* Pods-TodoListTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodoListTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TodoListTests/Pods-TodoListTests.debug.xcconfig"; sourceTree = ""; }; 59 | 488EE22E135075977EA2DFA9 /* Pods-TodoList.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodoList.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TodoList/Pods-TodoList.debug.xcconfig"; sourceTree = ""; }; 60 | 60A34AB7D1FE4AD58CA37B25 /* Pods-TodoList.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodoList.release.xcconfig"; path = "Pods/Target Support Files/Pods-TodoList/Pods-TodoList.release.xcconfig"; sourceTree = ""; }; 61 | A41302F21E7D9DC100B59EE8 /* TodoItemListViewModel_features.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoItemListViewModel_features.swift; sourceTree = ""; }; 62 | A41302F51E7DABC600B59EE8 /* MessageBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageBox.swift; sourceTree = ""; }; 63 | A41302F71E7DAE1500B59EE8 /* ServiceLocator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = ""; }; 64 | A41302F91E7DB3A800B59EE8 /* StubMessageBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubMessageBox.swift; sourceTree = ""; }; 65 | A44B28F11E750A4B0068BCB8 /* TodoItemListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoItemListViewModel.swift; sourceTree = ""; }; 66 | A44B28F31E750BD20068BCB8 /* NewTodoItemFormViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewTodoItemFormViewModel.swift; sourceTree = ""; }; 67 | A44B28F51E750D3A0068BCB8 /* ApplicationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationModel.swift; sourceTree = ""; }; 68 | A44B28F91E7514670068BCB8 /* NewTodoItemFormViewModel_specs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewTodoItemFormViewModel_specs.swift; sourceTree = ""; }; 69 | A44B28FB1E7517180068BCB8 /* RelayCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayCommand.swift; sourceTree = ""; }; 70 | A44B28FD1E751A2B0068BCB8 /* RelayCommand_specs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayCommand_specs.swift; sourceTree = ""; }; 71 | A44E807D1E744A8D00E41C77 /* TodoItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoItemViewModel.swift; sourceTree = ""; }; 72 | A44E807F1E7472A900E41C77 /* ObservableCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableCollection.swift; sourceTree = ""; }; 73 | A44E80811E74F8CE00E41C77 /* ObservableCollection_specs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableCollection_specs.swift; sourceTree = ""; }; 74 | A49242C71E75513800BBEA4E /* ViewModelLocator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelLocator.swift; sourceTree = ""; }; 75 | A4B90BDA1E754AAD00297ED2 /* TodoItemListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoItemListViewController.swift; sourceTree = ""; }; 76 | A4C5033A1E741BA200E1F149 /* TodoList.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TodoList.app; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | A4C5033D1E741BA200E1F149 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 78 | A4C503441E741BA200E1F149 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 79 | A4C503461E741BA200E1F149 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 80 | A4C503491E741BA200E1F149 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 81 | A4C5034B1E741BA200E1F149 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 82 | A4C503501E741BA200E1F149 /* TodoListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TodoListTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | A4C503541E741BA200E1F149 /* TodoItemViewModel_specs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoItemViewModel_specs.swift; sourceTree = ""; }; 84 | A4C503561E741BA200E1F149 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 85 | A4C5035B1E741BA200E1F149 /* TodoListUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TodoListUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 86 | A4C5035F1E741BA200E1F149 /* TodoListUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListUITests.swift; sourceTree = ""; }; 87 | A4C503611E741BA200E1F149 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 88 | A4FCD0E71E7552F700D14BBD /* NewTodoItemFormViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewTodoItemFormViewController.swift; sourceTree = ""; }; 89 | C1EB14034D3DCB48191FEABD /* Pods_TodoList.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TodoList.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 90 | D59ADD2DB44AE377C68590B3 /* Pods-TodoListUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TodoListUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TodoListUITests/Pods-TodoListUITests.debug.xcconfig"; sourceTree = ""; }; 91 | F4DC58928149C92522442E0F /* Pods_TodoListTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TodoListTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 92 | /* End PBXFileReference section */ 93 | 94 | /* Begin PBXFrameworksBuildPhase section */ 95 | A4C503371E741BA200E1F149 /* Frameworks */ = { 96 | isa = PBXFrameworksBuildPhase; 97 | buildActionMask = 2147483647; 98 | files = ( 99 | 4D120B8E221C84C0DD5895A5 /* Pods_TodoList.framework in Frameworks */, 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | A4C5034D1E741BA200E1F149 /* Frameworks */ = { 104 | isa = PBXFrameworksBuildPhase; 105 | buildActionMask = 2147483647; 106 | files = ( 107 | C1A79D4A86CB57BE764BD8C5 /* Pods_TodoListTests.framework in Frameworks */, 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | A4C503581E741BA200E1F149 /* Frameworks */ = { 112 | isa = PBXFrameworksBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | D170125F063A0BF80174D16F /* Pods_TodoListUITests.framework in Frameworks */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXFrameworksBuildPhase section */ 120 | 121 | /* Begin PBXGroup section */ 122 | A41302F01E7CF46300B59EE8 /* MVVM */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | A44E80811E74F8CE00E41C77 /* ObservableCollection_specs.swift */, 126 | A44B28FD1E751A2B0068BCB8 /* RelayCommand_specs.swift */, 127 | ); 128 | name = MVVM; 129 | sourceTree = ""; 130 | }; 131 | A41302F11E7CF47300B59EE8 /* ViewModels */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | A4C503541E741BA200E1F149 /* TodoItemViewModel_specs.swift */, 135 | A44B28F91E7514670068BCB8 /* NewTodoItemFormViewModel_specs.swift */, 136 | A41302F21E7D9DC100B59EE8 /* TodoItemListViewModel_features.swift */, 137 | A41302F91E7DB3A800B59EE8 /* StubMessageBox.swift */, 138 | ); 139 | name = ViewModels; 140 | sourceTree = ""; 141 | }; 142 | A41302F41E7DA2E900B59EE8 /* Services */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | A41302F51E7DABC600B59EE8 /* MessageBox.swift */, 146 | ); 147 | name = Services; 148 | sourceTree = ""; 149 | }; 150 | A45895ED1E75A465007E211D /* ViewModels */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | A44E807D1E744A8D00E41C77 /* TodoItemViewModel.swift */, 154 | A44B28F11E750A4B0068BCB8 /* TodoItemListViewModel.swift */, 155 | A44B28F31E750BD20068BCB8 /* NewTodoItemFormViewModel.swift */, 156 | A44B28F51E750D3A0068BCB8 /* ApplicationModel.swift */, 157 | ); 158 | name = ViewModels; 159 | sourceTree = ""; 160 | }; 161 | A45895EE1E75A480007E211D /* Views */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | A4C503431E741BA200E1F149 /* Main.storyboard */, 165 | A41302F71E7DAE1500B59EE8 /* ServiceLocator.swift */, 166 | A49242C71E75513800BBEA4E /* ViewModelLocator.swift */, 167 | A4B90BDA1E754AAD00297ED2 /* TodoItemListViewController.swift */, 168 | A4FCD0E71E7552F700D14BBD /* NewTodoItemFormViewController.swift */, 169 | ); 170 | name = Views; 171 | sourceTree = ""; 172 | }; 173 | A45895EF1E75A4AD007E211D /* MVVM */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | A44E807F1E7472A900E41C77 /* ObservableCollection.swift */, 177 | A44B28FB1E7517180068BCB8 /* RelayCommand.swift */, 178 | ); 179 | name = MVVM; 180 | sourceTree = ""; 181 | }; 182 | A4C503311E741BA100E1F149 = { 183 | isa = PBXGroup; 184 | children = ( 185 | A4C5033C1E741BA200E1F149 /* TodoList */, 186 | A4C503531E741BA200E1F149 /* TodoListTests */, 187 | A4C5035E1E741BA200E1F149 /* TodoListUITests */, 188 | A4C5033B1E741BA200E1F149 /* Products */, 189 | B0FEA73660BA24B4C7621641 /* Pods */, 190 | D1CF3DBCC3BC34393673EFFA /* Frameworks */, 191 | ); 192 | sourceTree = ""; 193 | }; 194 | A4C5033B1E741BA200E1F149 /* Products */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | A4C5033A1E741BA200E1F149 /* TodoList.app */, 198 | A4C503501E741BA200E1F149 /* TodoListTests.xctest */, 199 | A4C5035B1E741BA200E1F149 /* TodoListUITests.xctest */, 200 | ); 201 | name = Products; 202 | sourceTree = ""; 203 | }; 204 | A4C5033C1E741BA200E1F149 /* TodoList */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | A45895EF1E75A4AD007E211D /* MVVM */, 208 | A41302F41E7DA2E900B59EE8 /* Services */, 209 | A45895ED1E75A465007E211D /* ViewModels */, 210 | A45895EE1E75A480007E211D /* Views */, 211 | A4C5033D1E741BA200E1F149 /* AppDelegate.swift */, 212 | A4C503461E741BA200E1F149 /* Assets.xcassets */, 213 | A4C503481E741BA200E1F149 /* LaunchScreen.storyboard */, 214 | A4C5034B1E741BA200E1F149 /* Info.plist */, 215 | ); 216 | path = TodoList; 217 | sourceTree = ""; 218 | }; 219 | A4C503531E741BA200E1F149 /* TodoListTests */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | A41302F01E7CF46300B59EE8 /* MVVM */, 223 | A41302F11E7CF47300B59EE8 /* ViewModels */, 224 | A4C503561E741BA200E1F149 /* Info.plist */, 225 | ); 226 | path = TodoListTests; 227 | sourceTree = ""; 228 | }; 229 | A4C5035E1E741BA200E1F149 /* TodoListUITests */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | A4C5035F1E741BA200E1F149 /* TodoListUITests.swift */, 233 | A4C503611E741BA200E1F149 /* Info.plist */, 234 | ); 235 | path = TodoListUITests; 236 | sourceTree = ""; 237 | }; 238 | B0FEA73660BA24B4C7621641 /* Pods */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 488EE22E135075977EA2DFA9 /* Pods-TodoList.debug.xcconfig */, 242 | 60A34AB7D1FE4AD58CA37B25 /* Pods-TodoList.release.xcconfig */, 243 | 2FEF863441818AD5AA859010 /* Pods-TodoListTests.debug.xcconfig */, 244 | 2D797FAE6A9956E1441B2798 /* Pods-TodoListTests.release.xcconfig */, 245 | D59ADD2DB44AE377C68590B3 /* Pods-TodoListUITests.debug.xcconfig */, 246 | 1F05CF88C2D4591B3EA080E1 /* Pods-TodoListUITests.release.xcconfig */, 247 | ); 248 | name = Pods; 249 | sourceTree = ""; 250 | }; 251 | D1CF3DBCC3BC34393673EFFA /* Frameworks */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | C1EB14034D3DCB48191FEABD /* Pods_TodoList.framework */, 255 | F4DC58928149C92522442E0F /* Pods_TodoListTests.framework */, 256 | 24914B092EA65AADC9E21E9F /* Pods_TodoListUITests.framework */, 257 | ); 258 | name = Frameworks; 259 | sourceTree = ""; 260 | }; 261 | /* End PBXGroup section */ 262 | 263 | /* Begin PBXNativeTarget section */ 264 | A4C503391E741BA200E1F149 /* TodoList */ = { 265 | isa = PBXNativeTarget; 266 | buildConfigurationList = A4C503641E741BA200E1F149 /* Build configuration list for PBXNativeTarget "TodoList" */; 267 | buildPhases = ( 268 | 8E02CC23EA78E0CFE55E68D3 /* [CP] Check Pods Manifest.lock */, 269 | A4C503361E741BA200E1F149 /* Sources */, 270 | A4C503371E741BA200E1F149 /* Frameworks */, 271 | A4C503381E741BA200E1F149 /* Resources */, 272 | 987988FAC66CE0046143C4B5 /* [CP] Embed Pods Frameworks */, 273 | F3CEB6E21D374D5B34BA7FE1 /* [CP] Copy Pods Resources */, 274 | 159877F01E90CC8B00D29EE0 /* Run SwiftLint */, 275 | ); 276 | buildRules = ( 277 | ); 278 | dependencies = ( 279 | ); 280 | name = TodoList; 281 | productName = TodoList; 282 | productReference = A4C5033A1E741BA200E1F149 /* TodoList.app */; 283 | productType = "com.apple.product-type.application"; 284 | }; 285 | A4C5034F1E741BA200E1F149 /* TodoListTests */ = { 286 | isa = PBXNativeTarget; 287 | buildConfigurationList = A4C503671E741BA200E1F149 /* Build configuration list for PBXNativeTarget "TodoListTests" */; 288 | buildPhases = ( 289 | 98DEF6571DE355A1182CFCF0 /* [CP] Check Pods Manifest.lock */, 290 | A4C5034C1E741BA200E1F149 /* Sources */, 291 | A4C5034D1E741BA200E1F149 /* Frameworks */, 292 | A4C5034E1E741BA200E1F149 /* Resources */, 293 | 3A9E0B89788699E42880FA0A /* [CP] Embed Pods Frameworks */, 294 | 824790663F7813A94E31B7BF /* [CP] Copy Pods Resources */, 295 | ); 296 | buildRules = ( 297 | ); 298 | dependencies = ( 299 | A4C503521E741BA200E1F149 /* PBXTargetDependency */, 300 | ); 301 | name = TodoListTests; 302 | productName = TodoListTests; 303 | productReference = A4C503501E741BA200E1F149 /* TodoListTests.xctest */; 304 | productType = "com.apple.product-type.bundle.unit-test"; 305 | }; 306 | A4C5035A1E741BA200E1F149 /* TodoListUITests */ = { 307 | isa = PBXNativeTarget; 308 | buildConfigurationList = A4C5036A1E741BA200E1F149 /* Build configuration list for PBXNativeTarget "TodoListUITests" */; 309 | buildPhases = ( 310 | D51D8612AEB3BFBBA05634D1 /* [CP] Check Pods Manifest.lock */, 311 | A4C503571E741BA200E1F149 /* Sources */, 312 | A4C503581E741BA200E1F149 /* Frameworks */, 313 | A4C503591E741BA200E1F149 /* Resources */, 314 | 655C9AE11B49ED3A8CDBAE73 /* [CP] Embed Pods Frameworks */, 315 | C8B8EC5761C5D923656F00B2 /* [CP] Copy Pods Resources */, 316 | ); 317 | buildRules = ( 318 | ); 319 | dependencies = ( 320 | A4C5035D1E741BA200E1F149 /* PBXTargetDependency */, 321 | ); 322 | name = TodoListUITests; 323 | productName = TodoListUITests; 324 | productReference = A4C5035B1E741BA200E1F149 /* TodoListUITests.xctest */; 325 | productType = "com.apple.product-type.bundle.ui-testing"; 326 | }; 327 | /* End PBXNativeTarget section */ 328 | 329 | /* Begin PBXProject section */ 330 | A4C503321E741BA200E1F149 /* Project object */ = { 331 | isa = PBXProject; 332 | attributes = { 333 | LastSwiftUpdateCheck = 0820; 334 | LastUpgradeCheck = 0820; 335 | ORGANIZATIONNAME = Gyuwon; 336 | TargetAttributes = { 337 | A4C503391E741BA200E1F149 = { 338 | CreatedOnToolsVersion = 8.2.1; 339 | DevelopmentTeam = 6V373BY4WV; 340 | ProvisioningStyle = Automatic; 341 | }; 342 | A4C5034F1E741BA200E1F149 = { 343 | CreatedOnToolsVersion = 8.2.1; 344 | DevelopmentTeam = 6V373BY4WV; 345 | ProvisioningStyle = Automatic; 346 | TestTargetID = A4C503391E741BA200E1F149; 347 | }; 348 | A4C5035A1E741BA200E1F149 = { 349 | CreatedOnToolsVersion = 8.2.1; 350 | DevelopmentTeam = 6V373BY4WV; 351 | ProvisioningStyle = Automatic; 352 | TestTargetID = A4C503391E741BA200E1F149; 353 | }; 354 | }; 355 | }; 356 | buildConfigurationList = A4C503351E741BA200E1F149 /* Build configuration list for PBXProject "TodoList" */; 357 | compatibilityVersion = "Xcode 3.2"; 358 | developmentRegion = English; 359 | hasScannedForEncodings = 0; 360 | knownRegions = ( 361 | en, 362 | Base, 363 | ); 364 | mainGroup = A4C503311E741BA100E1F149; 365 | productRefGroup = A4C5033B1E741BA200E1F149 /* Products */; 366 | projectDirPath = ""; 367 | projectRoot = ""; 368 | targets = ( 369 | A4C503391E741BA200E1F149 /* TodoList */, 370 | A4C5034F1E741BA200E1F149 /* TodoListTests */, 371 | A4C5035A1E741BA200E1F149 /* TodoListUITests */, 372 | ); 373 | }; 374 | /* End PBXProject section */ 375 | 376 | /* Begin PBXResourcesBuildPhase section */ 377 | A4C503381E741BA200E1F149 /* Resources */ = { 378 | isa = PBXResourcesBuildPhase; 379 | buildActionMask = 2147483647; 380 | files = ( 381 | A4C5034A1E741BA200E1F149 /* LaunchScreen.storyboard in Resources */, 382 | A4C503471E741BA200E1F149 /* Assets.xcassets in Resources */, 383 | A4C503451E741BA200E1F149 /* Main.storyboard in Resources */, 384 | ); 385 | runOnlyForDeploymentPostprocessing = 0; 386 | }; 387 | A4C5034E1E741BA200E1F149 /* Resources */ = { 388 | isa = PBXResourcesBuildPhase; 389 | buildActionMask = 2147483647; 390 | files = ( 391 | ); 392 | runOnlyForDeploymentPostprocessing = 0; 393 | }; 394 | A4C503591E741BA200E1F149 /* Resources */ = { 395 | isa = PBXResourcesBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | ); 399 | runOnlyForDeploymentPostprocessing = 0; 400 | }; 401 | /* End PBXResourcesBuildPhase section */ 402 | 403 | /* Begin PBXShellScriptBuildPhase section */ 404 | 159877F01E90CC8B00D29EE0 /* Run SwiftLint */ = { 405 | isa = PBXShellScriptBuildPhase; 406 | buildActionMask = 2147483647; 407 | files = ( 408 | ); 409 | inputPaths = ( 410 | ); 411 | name = "Run SwiftLint"; 412 | outputPaths = ( 413 | ); 414 | runOnlyForDeploymentPostprocessing = 0; 415 | shellPath = /bin/sh; 416 | shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\""; 417 | }; 418 | 3A9E0B89788699E42880FA0A /* [CP] Embed Pods Frameworks */ = { 419 | isa = PBXShellScriptBuildPhase; 420 | buildActionMask = 2147483647; 421 | files = ( 422 | ); 423 | inputPaths = ( 424 | ); 425 | name = "[CP] Embed Pods Frameworks"; 426 | outputPaths = ( 427 | ); 428 | runOnlyForDeploymentPostprocessing = 0; 429 | shellPath = /bin/sh; 430 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TodoListTests/Pods-TodoListTests-frameworks.sh\"\n"; 431 | showEnvVarsInLog = 0; 432 | }; 433 | 655C9AE11B49ED3A8CDBAE73 /* [CP] Embed Pods Frameworks */ = { 434 | isa = PBXShellScriptBuildPhase; 435 | buildActionMask = 2147483647; 436 | files = ( 437 | ); 438 | inputPaths = ( 439 | ); 440 | name = "[CP] Embed Pods Frameworks"; 441 | outputPaths = ( 442 | ); 443 | runOnlyForDeploymentPostprocessing = 0; 444 | shellPath = /bin/sh; 445 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TodoListUITests/Pods-TodoListUITests-frameworks.sh\"\n"; 446 | showEnvVarsInLog = 0; 447 | }; 448 | 824790663F7813A94E31B7BF /* [CP] Copy Pods Resources */ = { 449 | isa = PBXShellScriptBuildPhase; 450 | buildActionMask = 2147483647; 451 | files = ( 452 | ); 453 | inputPaths = ( 454 | ); 455 | name = "[CP] Copy Pods Resources"; 456 | outputPaths = ( 457 | ); 458 | runOnlyForDeploymentPostprocessing = 0; 459 | shellPath = /bin/sh; 460 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TodoListTests/Pods-TodoListTests-resources.sh\"\n"; 461 | showEnvVarsInLog = 0; 462 | }; 463 | 8E02CC23EA78E0CFE55E68D3 /* [CP] Check Pods Manifest.lock */ = { 464 | isa = PBXShellScriptBuildPhase; 465 | buildActionMask = 2147483647; 466 | files = ( 467 | ); 468 | inputPaths = ( 469 | ); 470 | name = "[CP] Check Pods Manifest.lock"; 471 | outputPaths = ( 472 | ); 473 | runOnlyForDeploymentPostprocessing = 0; 474 | shellPath = /bin/sh; 475 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 476 | showEnvVarsInLog = 0; 477 | }; 478 | 987988FAC66CE0046143C4B5 /* [CP] Embed Pods Frameworks */ = { 479 | isa = PBXShellScriptBuildPhase; 480 | buildActionMask = 2147483647; 481 | files = ( 482 | ); 483 | inputPaths = ( 484 | ); 485 | name = "[CP] Embed Pods Frameworks"; 486 | outputPaths = ( 487 | ); 488 | runOnlyForDeploymentPostprocessing = 0; 489 | shellPath = /bin/sh; 490 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TodoList/Pods-TodoList-frameworks.sh\"\n"; 491 | showEnvVarsInLog = 0; 492 | }; 493 | 98DEF6571DE355A1182CFCF0 /* [CP] Check Pods Manifest.lock */ = { 494 | isa = PBXShellScriptBuildPhase; 495 | buildActionMask = 2147483647; 496 | files = ( 497 | ); 498 | inputPaths = ( 499 | ); 500 | name = "[CP] Check Pods Manifest.lock"; 501 | outputPaths = ( 502 | ); 503 | runOnlyForDeploymentPostprocessing = 0; 504 | shellPath = /bin/sh; 505 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 506 | showEnvVarsInLog = 0; 507 | }; 508 | C8B8EC5761C5D923656F00B2 /* [CP] Copy Pods Resources */ = { 509 | isa = PBXShellScriptBuildPhase; 510 | buildActionMask = 2147483647; 511 | files = ( 512 | ); 513 | inputPaths = ( 514 | ); 515 | name = "[CP] Copy Pods Resources"; 516 | outputPaths = ( 517 | ); 518 | runOnlyForDeploymentPostprocessing = 0; 519 | shellPath = /bin/sh; 520 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TodoListUITests/Pods-TodoListUITests-resources.sh\"\n"; 521 | showEnvVarsInLog = 0; 522 | }; 523 | D51D8612AEB3BFBBA05634D1 /* [CP] Check Pods Manifest.lock */ = { 524 | isa = PBXShellScriptBuildPhase; 525 | buildActionMask = 2147483647; 526 | files = ( 527 | ); 528 | inputPaths = ( 529 | ); 530 | name = "[CP] Check Pods Manifest.lock"; 531 | outputPaths = ( 532 | ); 533 | runOnlyForDeploymentPostprocessing = 0; 534 | shellPath = /bin/sh; 535 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 536 | showEnvVarsInLog = 0; 537 | }; 538 | F3CEB6E21D374D5B34BA7FE1 /* [CP] Copy Pods Resources */ = { 539 | isa = PBXShellScriptBuildPhase; 540 | buildActionMask = 2147483647; 541 | files = ( 542 | ); 543 | inputPaths = ( 544 | ); 545 | name = "[CP] Copy Pods Resources"; 546 | outputPaths = ( 547 | ); 548 | runOnlyForDeploymentPostprocessing = 0; 549 | shellPath = /bin/sh; 550 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-TodoList/Pods-TodoList-resources.sh\"\n"; 551 | showEnvVarsInLog = 0; 552 | }; 553 | /* End PBXShellScriptBuildPhase section */ 554 | 555 | /* Begin PBXSourcesBuildPhase section */ 556 | A4C503361E741BA200E1F149 /* Sources */ = { 557 | isa = PBXSourcesBuildPhase; 558 | buildActionMask = 2147483647; 559 | files = ( 560 | A41302F81E7DAE1500B59EE8 /* ServiceLocator.swift in Sources */, 561 | A44B28F61E750D3A0068BCB8 /* ApplicationModel.swift in Sources */, 562 | A44B28F41E750BD20068BCB8 /* NewTodoItemFormViewModel.swift in Sources */, 563 | A4FCD0E81E7552F700D14BBD /* NewTodoItemFormViewController.swift in Sources */, 564 | A4C5033E1E741BA200E1F149 /* AppDelegate.swift in Sources */, 565 | A44B28FC1E7517180068BCB8 /* RelayCommand.swift in Sources */, 566 | A44E80801E7472A900E41C77 /* ObservableCollection.swift in Sources */, 567 | A49242C81E75513800BBEA4E /* ViewModelLocator.swift in Sources */, 568 | A44B28F21E750A4B0068BCB8 /* TodoItemListViewModel.swift in Sources */, 569 | A41302F61E7DABC600B59EE8 /* MessageBox.swift in Sources */, 570 | A4B90BDB1E754AAD00297ED2 /* TodoItemListViewController.swift in Sources */, 571 | A44E807E1E744A8D00E41C77 /* TodoItemViewModel.swift in Sources */, 572 | ); 573 | runOnlyForDeploymentPostprocessing = 0; 574 | }; 575 | A4C5034C1E741BA200E1F149 /* Sources */ = { 576 | isa = PBXSourcesBuildPhase; 577 | buildActionMask = 2147483647; 578 | files = ( 579 | A41302F31E7D9DC100B59EE8 /* TodoItemListViewModel_features.swift in Sources */, 580 | A44B28FE1E751A2B0068BCB8 /* RelayCommand_specs.swift in Sources */, 581 | A44B28FA1E7514670068BCB8 /* NewTodoItemFormViewModel_specs.swift in Sources */, 582 | A44E80821E74F8CE00E41C77 /* ObservableCollection_specs.swift in Sources */, 583 | A4C503551E741BA200E1F149 /* TodoItemViewModel_specs.swift in Sources */, 584 | A41302FA1E7DB3A800B59EE8 /* StubMessageBox.swift in Sources */, 585 | ); 586 | runOnlyForDeploymentPostprocessing = 0; 587 | }; 588 | A4C503571E741BA200E1F149 /* Sources */ = { 589 | isa = PBXSourcesBuildPhase; 590 | buildActionMask = 2147483647; 591 | files = ( 592 | A4C503601E741BA200E1F149 /* TodoListUITests.swift in Sources */, 593 | ); 594 | runOnlyForDeploymentPostprocessing = 0; 595 | }; 596 | /* End PBXSourcesBuildPhase section */ 597 | 598 | /* Begin PBXTargetDependency section */ 599 | A4C503521E741BA200E1F149 /* PBXTargetDependency */ = { 600 | isa = PBXTargetDependency; 601 | target = A4C503391E741BA200E1F149 /* TodoList */; 602 | targetProxy = A4C503511E741BA200E1F149 /* PBXContainerItemProxy */; 603 | }; 604 | A4C5035D1E741BA200E1F149 /* PBXTargetDependency */ = { 605 | isa = PBXTargetDependency; 606 | target = A4C503391E741BA200E1F149 /* TodoList */; 607 | targetProxy = A4C5035C1E741BA200E1F149 /* PBXContainerItemProxy */; 608 | }; 609 | /* End PBXTargetDependency section */ 610 | 611 | /* Begin PBXVariantGroup section */ 612 | A4C503431E741BA200E1F149 /* Main.storyboard */ = { 613 | isa = PBXVariantGroup; 614 | children = ( 615 | A4C503441E741BA200E1F149 /* Base */, 616 | ); 617 | name = Main.storyboard; 618 | sourceTree = ""; 619 | }; 620 | A4C503481E741BA200E1F149 /* LaunchScreen.storyboard */ = { 621 | isa = PBXVariantGroup; 622 | children = ( 623 | A4C503491E741BA200E1F149 /* Base */, 624 | ); 625 | name = LaunchScreen.storyboard; 626 | sourceTree = ""; 627 | }; 628 | /* End PBXVariantGroup section */ 629 | 630 | /* Begin XCBuildConfiguration section */ 631 | A4C503621E741BA200E1F149 /* Debug */ = { 632 | isa = XCBuildConfiguration; 633 | buildSettings = { 634 | ALWAYS_SEARCH_USER_PATHS = NO; 635 | CLANG_ANALYZER_NONNULL = YES; 636 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 637 | CLANG_CXX_LIBRARY = "libc++"; 638 | CLANG_ENABLE_MODULES = YES; 639 | CLANG_ENABLE_OBJC_ARC = YES; 640 | CLANG_WARN_BOOL_CONVERSION = YES; 641 | CLANG_WARN_CONSTANT_CONVERSION = YES; 642 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 643 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 644 | CLANG_WARN_EMPTY_BODY = YES; 645 | CLANG_WARN_ENUM_CONVERSION = YES; 646 | CLANG_WARN_INFINITE_RECURSION = YES; 647 | CLANG_WARN_INT_CONVERSION = YES; 648 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 649 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 650 | CLANG_WARN_UNREACHABLE_CODE = YES; 651 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 652 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 653 | COPY_PHASE_STRIP = NO; 654 | DEBUG_INFORMATION_FORMAT = dwarf; 655 | ENABLE_STRICT_OBJC_MSGSEND = YES; 656 | ENABLE_TESTABILITY = YES; 657 | GCC_C_LANGUAGE_STANDARD = gnu99; 658 | GCC_DYNAMIC_NO_PIC = NO; 659 | GCC_NO_COMMON_BLOCKS = YES; 660 | GCC_OPTIMIZATION_LEVEL = 0; 661 | GCC_PREPROCESSOR_DEFINITIONS = ( 662 | "DEBUG=1", 663 | "$(inherited)", 664 | ); 665 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 666 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 667 | GCC_WARN_UNDECLARED_SELECTOR = YES; 668 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 669 | GCC_WARN_UNUSED_FUNCTION = YES; 670 | GCC_WARN_UNUSED_VARIABLE = YES; 671 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 672 | MTL_ENABLE_DEBUG_INFO = YES; 673 | ONLY_ACTIVE_ARCH = YES; 674 | SDKROOT = iphoneos; 675 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 676 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 677 | TARGETED_DEVICE_FAMILY = "1,2"; 678 | }; 679 | name = Debug; 680 | }; 681 | A4C503631E741BA200E1F149 /* Release */ = { 682 | isa = XCBuildConfiguration; 683 | buildSettings = { 684 | ALWAYS_SEARCH_USER_PATHS = NO; 685 | CLANG_ANALYZER_NONNULL = YES; 686 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 687 | CLANG_CXX_LIBRARY = "libc++"; 688 | CLANG_ENABLE_MODULES = YES; 689 | CLANG_ENABLE_OBJC_ARC = YES; 690 | CLANG_WARN_BOOL_CONVERSION = YES; 691 | CLANG_WARN_CONSTANT_CONVERSION = YES; 692 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 693 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 694 | CLANG_WARN_EMPTY_BODY = YES; 695 | CLANG_WARN_ENUM_CONVERSION = YES; 696 | CLANG_WARN_INFINITE_RECURSION = YES; 697 | CLANG_WARN_INT_CONVERSION = YES; 698 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 699 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 700 | CLANG_WARN_UNREACHABLE_CODE = YES; 701 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 702 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 703 | COPY_PHASE_STRIP = NO; 704 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 705 | ENABLE_NS_ASSERTIONS = NO; 706 | ENABLE_STRICT_OBJC_MSGSEND = YES; 707 | GCC_C_LANGUAGE_STANDARD = gnu99; 708 | GCC_NO_COMMON_BLOCKS = YES; 709 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 710 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 711 | GCC_WARN_UNDECLARED_SELECTOR = YES; 712 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 713 | GCC_WARN_UNUSED_FUNCTION = YES; 714 | GCC_WARN_UNUSED_VARIABLE = YES; 715 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 716 | MTL_ENABLE_DEBUG_INFO = NO; 717 | SDKROOT = iphoneos; 718 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 719 | TARGETED_DEVICE_FAMILY = "1,2"; 720 | VALIDATE_PRODUCT = YES; 721 | }; 722 | name = Release; 723 | }; 724 | A4C503651E741BA200E1F149 /* Debug */ = { 725 | isa = XCBuildConfiguration; 726 | baseConfigurationReference = 488EE22E135075977EA2DFA9 /* Pods-TodoList.debug.xcconfig */; 727 | buildSettings = { 728 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 729 | DEVELOPMENT_TEAM = 6V373BY4WV; 730 | INFOPLIST_FILE = TodoList/Info.plist; 731 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 732 | PRODUCT_BUNDLE_IDENTIFIER = gyuwon.TodoList; 733 | PRODUCT_NAME = "$(TARGET_NAME)"; 734 | SWIFT_VERSION = 3.0; 735 | }; 736 | name = Debug; 737 | }; 738 | A4C503661E741BA200E1F149 /* Release */ = { 739 | isa = XCBuildConfiguration; 740 | baseConfigurationReference = 60A34AB7D1FE4AD58CA37B25 /* Pods-TodoList.release.xcconfig */; 741 | buildSettings = { 742 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 743 | DEVELOPMENT_TEAM = 6V373BY4WV; 744 | INFOPLIST_FILE = TodoList/Info.plist; 745 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 746 | PRODUCT_BUNDLE_IDENTIFIER = gyuwon.TodoList; 747 | PRODUCT_NAME = "$(TARGET_NAME)"; 748 | SWIFT_VERSION = 3.0; 749 | }; 750 | name = Release; 751 | }; 752 | A4C503681E741BA200E1F149 /* Debug */ = { 753 | isa = XCBuildConfiguration; 754 | baseConfigurationReference = 2FEF863441818AD5AA859010 /* Pods-TodoListTests.debug.xcconfig */; 755 | buildSettings = { 756 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 757 | BUNDLE_LOADER = "$(TEST_HOST)"; 758 | DEVELOPMENT_TEAM = 6V373BY4WV; 759 | INFOPLIST_FILE = TodoListTests/Info.plist; 760 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 761 | PRODUCT_BUNDLE_IDENTIFIER = gyuwon.TodoListTests; 762 | PRODUCT_NAME = "$(TARGET_NAME)"; 763 | SWIFT_VERSION = 3.0; 764 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TodoList.app/TodoList"; 765 | }; 766 | name = Debug; 767 | }; 768 | A4C503691E741BA200E1F149 /* Release */ = { 769 | isa = XCBuildConfiguration; 770 | baseConfigurationReference = 2D797FAE6A9956E1441B2798 /* Pods-TodoListTests.release.xcconfig */; 771 | buildSettings = { 772 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 773 | BUNDLE_LOADER = "$(TEST_HOST)"; 774 | DEVELOPMENT_TEAM = 6V373BY4WV; 775 | INFOPLIST_FILE = TodoListTests/Info.plist; 776 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 777 | PRODUCT_BUNDLE_IDENTIFIER = gyuwon.TodoListTests; 778 | PRODUCT_NAME = "$(TARGET_NAME)"; 779 | SWIFT_VERSION = 3.0; 780 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TodoList.app/TodoList"; 781 | }; 782 | name = Release; 783 | }; 784 | A4C5036B1E741BA200E1F149 /* Debug */ = { 785 | isa = XCBuildConfiguration; 786 | baseConfigurationReference = D59ADD2DB44AE377C68590B3 /* Pods-TodoListUITests.debug.xcconfig */; 787 | buildSettings = { 788 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 789 | DEVELOPMENT_TEAM = 6V373BY4WV; 790 | INFOPLIST_FILE = TodoListUITests/Info.plist; 791 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 792 | PRODUCT_BUNDLE_IDENTIFIER = gyuwon.TodoListUITests; 793 | PRODUCT_NAME = "$(TARGET_NAME)"; 794 | SWIFT_VERSION = 3.0; 795 | TEST_TARGET_NAME = TodoList; 796 | }; 797 | name = Debug; 798 | }; 799 | A4C5036C1E741BA200E1F149 /* Release */ = { 800 | isa = XCBuildConfiguration; 801 | baseConfigurationReference = 1F05CF88C2D4591B3EA080E1 /* Pods-TodoListUITests.release.xcconfig */; 802 | buildSettings = { 803 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 804 | DEVELOPMENT_TEAM = 6V373BY4WV; 805 | INFOPLIST_FILE = TodoListUITests/Info.plist; 806 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 807 | PRODUCT_BUNDLE_IDENTIFIER = gyuwon.TodoListUITests; 808 | PRODUCT_NAME = "$(TARGET_NAME)"; 809 | SWIFT_VERSION = 3.0; 810 | TEST_TARGET_NAME = TodoList; 811 | }; 812 | name = Release; 813 | }; 814 | /* End XCBuildConfiguration section */ 815 | 816 | /* Begin XCConfigurationList section */ 817 | A4C503351E741BA200E1F149 /* Build configuration list for PBXProject "TodoList" */ = { 818 | isa = XCConfigurationList; 819 | buildConfigurations = ( 820 | A4C503621E741BA200E1F149 /* Debug */, 821 | A4C503631E741BA200E1F149 /* Release */, 822 | ); 823 | defaultConfigurationIsVisible = 0; 824 | defaultConfigurationName = Release; 825 | }; 826 | A4C503641E741BA200E1F149 /* Build configuration list for PBXNativeTarget "TodoList" */ = { 827 | isa = XCConfigurationList; 828 | buildConfigurations = ( 829 | A4C503651E741BA200E1F149 /* Debug */, 830 | A4C503661E741BA200E1F149 /* Release */, 831 | ); 832 | defaultConfigurationIsVisible = 0; 833 | defaultConfigurationName = Release; 834 | }; 835 | A4C503671E741BA200E1F149 /* Build configuration list for PBXNativeTarget "TodoListTests" */ = { 836 | isa = XCConfigurationList; 837 | buildConfigurations = ( 838 | A4C503681E741BA200E1F149 /* Debug */, 839 | A4C503691E741BA200E1F149 /* Release */, 840 | ); 841 | defaultConfigurationIsVisible = 0; 842 | defaultConfigurationName = Release; 843 | }; 844 | A4C5036A1E741BA200E1F149 /* Build configuration list for PBXNativeTarget "TodoListUITests" */ = { 845 | isa = XCConfigurationList; 846 | buildConfigurations = ( 847 | A4C5036B1E741BA200E1F149 /* Debug */, 848 | A4C5036C1E741BA200E1F149 /* Release */, 849 | ); 850 | defaultConfigurationIsVisible = 0; 851 | defaultConfigurationName = Release; 852 | }; 853 | /* End XCConfigurationList section */ 854 | }; 855 | rootObject = A4C503321E741BA200E1F149 /* Project object */; 856 | } 857 | -------------------------------------------------------------------------------- /TodoList.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TodoList.xcodeproj/xcuserdata/gyuwon.xcuserdatad/xcschemes/TodoList.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /TodoList.xcodeproj/xcuserdata/gyuwon.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TodoList.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | A4C503391E741BA200E1F149 16 | 17 | primary 18 | 19 | 20 | A4C5034F1E741BA200E1F149 21 | 22 | primary 23 | 24 | 25 | A4C5035A1E741BA200E1F149 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /TodoList.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TodoList/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/11/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | 20 | func applicationWillResignActive(_ application: UIApplication) { 21 | } 22 | 23 | func applicationDidEnterBackground(_ application: UIApplication) { 24 | } 25 | 26 | func applicationWillEnterForeground(_ application: UIApplication) { 27 | } 28 | 29 | func applicationDidBecomeActive(_ application: UIApplication) { 30 | } 31 | 32 | func applicationWillTerminate(_ application: UIApplication) { 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /TodoList/ApplicationModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoListApplicationModel.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ApplicationModel { 12 | 13 | let itemList: TodoItemListViewModel 14 | let newItemForm: NewTodoItemFormViewModel 15 | 16 | init(messageBox: MessageBox) { 17 | itemList = TodoItemListViewModel(messageBox: messageBox) 18 | newItemForm = NewTodoItemFormViewModel(itemList: itemList) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /TodoList/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /TodoList/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /TodoList/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 81 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 103 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /TodoList/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarTintParameters 32 | 33 | UINavigationBar 34 | 35 | Style 36 | UIBarStyleDefault 37 | Translucent 38 | 39 | 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /TodoList/MessageBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBox.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/19/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol MessageBox { 12 | 13 | func confirm(title: String, message: String, confirmText: String, cancelText: String, destructive: Bool, confirmed: @escaping (Void) -> Void) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /TodoList/NewTodoItemFormViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewTodoItemFormViewController.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | class NewTodoItemFormViewController: UIViewController { 14 | 15 | @IBOutlet weak var descriptionField: UITextField! 16 | @IBOutlet weak var submitButton: UIButton! 17 | private var _binding: Disposable? 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | _binding?.dispose() 23 | 24 | let viewModel: NewTodoItemFormViewModel = ViewModelLocator.applicationModel.newItemForm 25 | 26 | let fromViewModel = CompositeDisposable(disposables: [ 27 | viewModel.propertyChanged.subscribe(onNext: { [weak self] propertyName in 28 | switch propertyName { 29 | case "description": 30 | self?.descriptionField.text = viewModel.description 31 | break 32 | default: 33 | break 34 | } 35 | }), 36 | viewModel.submit.canExecuteChanged.subscribe(onNext: { [weak self] _ in 37 | let canExecuteCommand: Bool = viewModel.submit.canExecute() 38 | self?.submitButton.isEnabled = canExecuteCommand 39 | }) 40 | ]) 41 | 42 | let fromView = CompositeDisposable(disposables: [ 43 | descriptionField.rx.text.subscribe(onNext: { value in viewModel.description = value! }), 44 | submitButton.rx.controlEvent(UIControlEvents.touchUpInside).subscribe(onNext: { [weak self] _ in 45 | viewModel.submit.execute() 46 | self?.dismiss(animated: true) 47 | }) 48 | ]) 49 | 50 | _binding = CompositeDisposable(disposables: [fromViewModel, fromView]) 51 | 52 | descriptionField.text = viewModel.description 53 | } 54 | 55 | override func didReceiveMemoryWarning() { 56 | super.didReceiveMemoryWarning() 57 | } 58 | 59 | @IBAction func onCancelButtonTouchUpInside(_ sender: Any) { 60 | self.dismiss(animated: true) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /TodoList/NewTodoItemFormViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewTodoItemViewModel.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | class NewTodoItemFormViewModel { 13 | 14 | private let _propertyChanged = PublishSubject() 15 | private let _itemList: TodoItemListViewModel 16 | private lazy var _submit: RelayCommand = RelayCommand( 17 | execute: self.executeSubmit, 18 | canExecute: self.canExecuteSubmit) 19 | 20 | init(itemList: TodoItemListViewModel) { 21 | _itemList = itemList 22 | } 23 | 24 | var propertyChanged: Observable { return _propertyChanged } 25 | 26 | var description: String = "" { 27 | didSet { 28 | _propertyChanged.onNext("description") 29 | _submit.raiseCanExecuteChanged() 30 | } 31 | } 32 | 33 | var submit: RelayCommand { return _submit } 34 | 35 | private func executeSubmit(parameter: Any?) { 36 | _itemList.items.append(item: TodoItemViewModel(description: self.description)) 37 | description = "" 38 | } 39 | 40 | private func canExecuteSubmit(parameter: Any?) -> Bool { 41 | return description != "" 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /TodoList/ObservableCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableCollection.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | enum CollectionChanged { 13 | 14 | case itemsAdded(Int, [T]) 15 | case itemsRemoved(Int, [T]) 16 | case reset 17 | 18 | } 19 | 20 | class ObservableCollection: Collection { 21 | 22 | private var _collection = [T]() 23 | private var _collectionChanged = PublishSubject>() 24 | 25 | var startIndex: Int { return _collection.startIndex } 26 | var endIndex: Int { return _collection.endIndex } 27 | subscript(position: Int) -> T { return _collection[position] } 28 | var collectionChanged: Observable> { return _collectionChanged } 29 | 30 | func index(after index: Int) -> Int { 31 | guard index < endIndex else { fatalError("The parameter after must be less than endIndex.") } 32 | return index + 1 33 | } 34 | 35 | func append(item: T) { 36 | let location = _collection.count 37 | _collection.append(item) 38 | _collectionChanged.onNext(CollectionChanged.itemsAdded(location, [item])) 39 | } 40 | 41 | func remove(at index: Int) { 42 | let items = [_collection[index]] 43 | _collection.remove(at: index) 44 | _collectionChanged.onNext(CollectionChanged.itemsRemoved(index, items)) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /TodoList/RelayCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | class RelayCommand { 13 | 14 | private let _execute: (Any?) -> Void 15 | private let _canExecute: (Any?) -> Bool 16 | private let _canExecuteChanged = PublishSubject() 17 | 18 | init(execute: @escaping (Any?) -> Void) { 19 | _execute = execute 20 | _canExecute = { _ in true } 21 | } 22 | 23 | init(execute: @escaping (Any?) -> Void, canExecute: @escaping (Any?) -> Bool) { 24 | _execute = execute 25 | _canExecute = canExecute 26 | } 27 | 28 | var canExecuteChanged: Observable { return _canExecuteChanged } 29 | 30 | func execute(parameter: Any? = nil) { 31 | if canExecute(parameter: parameter) { 32 | _execute(parameter) 33 | } 34 | } 35 | 36 | func canExecute(parameter: Any? = nil) -> Bool { 37 | return _canExecute(parameter) 38 | } 39 | 40 | func raiseCanExecuteChanged() { 41 | _canExecuteChanged.onNext(Void()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TodoList/ServiceLocator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceLocator.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/19/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class UIAlertMessageBox: MessageBox { 13 | 14 | var currentViewController: UIViewController? 15 | 16 | func confirm(title: String, message: String, confirmText: String, cancelText: String, destructive: Bool, confirmed: @escaping (Void) -> Void) { 17 | guard let currentViewController = currentViewController else { return } 18 | 19 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 20 | alert.addAction(UIAlertAction(title: cancelText, style: .default)) 21 | alert.addAction(UIAlertAction(title: confirmText, style: destructive ? .destructive : .default) { (_) -> Void in 22 | confirmed() 23 | }) 24 | currentViewController.present(alert, animated: true, completion: nil) 25 | } 26 | 27 | } 28 | 29 | struct ServiceLocator { 30 | 31 | static var messageBox = UIAlertMessageBox() 32 | 33 | } 34 | -------------------------------------------------------------------------------- /TodoList/TodoItemListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoItemListViewController.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | class TodoItemCell: UITableViewCell { 14 | 15 | @IBOutlet var descriptionLabel: UILabel! 16 | 17 | private var _binding: Disposable? 18 | 19 | var dataContext: TodoItemViewModel? = nil { 20 | didSet { 21 | _binding?.dispose() 22 | 23 | guard let dataContext = dataContext else { 24 | descriptionLabel.text = "" 25 | return 26 | } 27 | 28 | let fromViewModel = dataContext.propertyChanged.subscribe(onNext: { [weak self] propertyName in 29 | switch propertyName { 30 | case "description": 31 | self?.descriptionLabel.text = self?.dataContext!.description 32 | break 33 | default: 34 | break 35 | } 36 | }) 37 | 38 | _binding = CompositeDisposable(disposables: [fromViewModel]) 39 | 40 | descriptionLabel.text = dataContext.description 41 | } 42 | } 43 | 44 | } 45 | 46 | class TodoItemListViewController: UITableViewController { 47 | 48 | private var _viewModel: TodoItemListViewModel { 49 | return ViewModelLocator.applicationModel.itemList 50 | } 51 | 52 | private var _binding: Disposable? 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | 57 | _binding?.dispose() 58 | 59 | _binding = _viewModel.items.collectionChanged.subscribe(onNext: { [weak self] _ in self?.tableView.reloadData() }) 60 | } 61 | 62 | override func viewDidAppear(_ animated: Bool) { 63 | ServiceLocator.messageBox.currentViewController = self 64 | } 65 | 66 | override func viewWillDisappear(_ animated: Bool) { 67 | ServiceLocator.messageBox.currentViewController = nil 68 | } 69 | 70 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 71 | return _viewModel.items.count 72 | } 73 | 74 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 75 | let cell = tableView.dequeueReusableCell(withIdentifier: "todoItemCell", for: indexPath) 76 | 77 | if let cell = cell as? TodoItemCell { 78 | let itemViewModel = _viewModel.items[indexPath.row] 79 | cell.dataContext = itemViewModel 80 | } 81 | 82 | return cell 83 | } 84 | 85 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 86 | _viewModel.deleteItem.execute(parameter: indexPath.row) 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /TodoList/TodoItemListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoListViewModel.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TodoItemListViewModel { 12 | 13 | private let _messageBox: MessageBox 14 | private lazy var _deleteItem: RelayCommand = RelayCommand(execute: self.executeDeleteItem) 15 | 16 | let items = ObservableCollection() 17 | 18 | init(messageBox: MessageBox) { 19 | _messageBox = messageBox 20 | } 21 | 22 | var deleteItem: RelayCommand { return _deleteItem } 23 | 24 | private func executeDeleteItem(parameter: Any?) { 25 | guard let index = parameter as? Int else { return } 26 | 27 | _messageBox.confirm( 28 | title: "Delete Todo Item?", 29 | message: "Description: \"" + items[index].description + "\"", 30 | confirmText: "Delete", 31 | cancelText: "Cancel", 32 | destructive: true, 33 | confirmed: { _ in self.items.remove(at: index) 34 | }) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /TodoList/TodoItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoItemViewModel.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | class TodoItemViewModel { 13 | 14 | private let _propertyChanged = PublishSubject() 15 | 16 | var description: String { 17 | didSet { 18 | _propertyChanged.onNext("description") 19 | } 20 | } 21 | 22 | var isComplete: Bool = false { 23 | didSet { 24 | _propertyChanged.onNext("isComplete") 25 | } 26 | } 27 | 28 | init(description: String) { 29 | self.description = description 30 | } 31 | 32 | var propertyChanged: Observable { return _propertyChanged } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /TodoList/ViewModelLocator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelLocator.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ViewModelLocator { 12 | 13 | static let applicationModel = ApplicationModel(messageBox: ServiceLocator.messageBox) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /TodoListTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TodoListTests/NewTodoItemFormViewModel_specs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewTodoItemFormViewModel_specs.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import RxSwift 11 | @testable import TodoList 12 | 13 | class NewTodoItemFormViewModel_specs: XCTestCase { 14 | 15 | func test_description_setter_raises_propertyChanged_correctly() { 16 | // Arrange 17 | let sut = NewTodoItemFormViewModel(itemList: TodoItemListViewModel(messageBox: StubMessageBox())) 18 | var events = [String]() 19 | _ = sut.propertyChanged.subscribe(onNext: { event in events.append(event) }) 20 | 21 | // Act 22 | sut.description = "foo" 23 | 24 | // Assert 25 | XCTAssertEqual(1, events.count) 26 | XCTAssertEqual("description", events[0]) 27 | } 28 | 29 | func test_submit_adds_new_item_to_list() { 30 | // Arrange 31 | let itemList = TodoItemListViewModel(messageBox: StubMessageBox()) 32 | let sut = NewTodoItemFormViewModel(itemList: itemList) 33 | let description: String = UUID().uuidString 34 | sut.description = description 35 | 36 | // Act 37 | sut.submit.execute() 38 | 39 | // Assert 40 | XCTAssertEqual(1, itemList.items.count) 41 | XCTAssertEqual(description, itemList.items[0].description) 42 | } 43 | 44 | func test_submit_clears_description_field() { 45 | // Arrange 46 | let sut = NewTodoItemFormViewModel(itemList: TodoItemListViewModel(messageBox: StubMessageBox())) 47 | sut.description = UUID().uuidString 48 | 49 | // Act 50 | sut.submit.execute() 51 | 52 | // Assert 53 | XCTAssertEqual("", sut.description) 54 | } 55 | 56 | func test_cannot_execute_submit_if_description_is_empty() { 57 | // Arrange 58 | let sut = NewTodoItemFormViewModel(itemList: TodoItemListViewModel(messageBox: StubMessageBox())) 59 | sut.description = "" 60 | 61 | // Act 62 | let actual: Bool = sut.submit.canExecute() 63 | 64 | // Assert 65 | XCTAssertEqual(false, actual) 66 | } 67 | 68 | func test_description_setter_raises_canExecuteChanged_of_submit_command() { 69 | // Arrange 70 | let sut = NewTodoItemFormViewModel(itemList: TodoItemListViewModel(messageBox: StubMessageBox())) 71 | var monitor: Int = 0 72 | _ = sut.submit.canExecuteChanged.subscribe(onNext: { monitor = monitor + 1 }) 73 | 74 | // Act 75 | sut.description = UUID().uuidString 76 | 77 | // Assert 78 | XCTAssertEqual(1, monitor) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /TodoListTests/ObservableCollection_specs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableCollection_specs.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import RxSwift 11 | @testable import TodoList 12 | 13 | class ObservableCollection_specs: XCTestCase { 14 | 15 | func test_append_inserts_new_item_at_the_end_correctly() { 16 | // Arrange 17 | let sut = ObservableCollection() 18 | let item = UUID().uuidString 19 | 20 | // Act 21 | sut.append(item: item) 22 | 23 | // Assert 24 | XCTAssertEqual(1, sut.count) 25 | let actual: String = sut[0] 26 | XCTAssertEqual(item, actual) 27 | } 28 | 29 | func test_append_raises_collectionChanged_event_correctly() { 30 | // Arrange 31 | let sut = ObservableCollection() 32 | sut.append(item: UUID().uuidString) 33 | let item = UUID().uuidString 34 | var events = [CollectionChanged]() 35 | _ = sut.collectionChanged.subscribe(onNext: { event in events.append(event) }) 36 | 37 | // Act 38 | sut.append(item: item) 39 | 40 | // Assert 41 | XCTAssertEqual(1, events.count) 42 | switch events[0] { 43 | case .itemsAdded(let location, let items): 44 | XCTAssertEqual(1, location) 45 | XCTAssertEqual(1, items.count) 46 | XCTAssertEqual(item, items[0]) 47 | break 48 | default: 49 | XCTFail("collectionChanged event is not raised correctly.") 50 | } 51 | } 52 | 53 | func test_remove_at_removes_item_at_the_specified_index() { 54 | // Arrange 55 | var data = [UUID().uuidString, UUID().uuidString, UUID().uuidString] 56 | let sut = ObservableCollection() 57 | for i in 0..<3 { 58 | sut.append(item: data[i]) 59 | } 60 | let index = Int(arc4random_uniform(UInt32(sut.count))) 61 | 62 | // Act 63 | sut.remove(at: index) 64 | 65 | // Assert 66 | data.remove(at: index) 67 | XCTAssertEqual(data.count, sut.count) 68 | for i in 0..() 77 | for i in 0..<3 { 78 | sut.append(item: data[i]) 79 | } 80 | var events = [CollectionChanged]() 81 | _ = sut.collectionChanged.subscribe(onNext: { event in events.append(event) }) 82 | let index = Int(arc4random_uniform(UInt32(sut.count))) 83 | 84 | // Act 85 | sut.remove(at: index) 86 | 87 | // Assert 88 | XCTAssertEqual(1, events.count) 89 | switch events[0] { 90 | case .itemsRemoved(let location, let items): 91 | XCTAssertEqual(index, location) 92 | XCTAssertEqual(1, items.count) 93 | XCTAssertEqual(data[index], items[0]) 94 | break 95 | default: 96 | XCTFail("collectionChanged event is not raised correctly.") 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /TodoListTests/RelayCommand_specs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelayCommand_specs.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/12/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import RxSwift 11 | @testable import TodoList 12 | 13 | class RelayCommand_specs: XCTestCase { 14 | 15 | func test_execute_relays_to_function() { 16 | // Arrange 17 | var monitor: Int = 0 18 | let sut = RelayCommand(execute: { _ in monitor = monitor + 1 }) 19 | 20 | // Act 21 | sut.execute() 22 | 23 | // Assert 24 | XCTAssertEqual(1, monitor) 25 | } 26 | 27 | func test_canExecute_relays_to_function() { 28 | // Arrange 29 | let expected = false 30 | let sut = RelayCommand(execute: { Void in }, canExecute: { Void in expected }) 31 | 32 | // Act 33 | let actual: Bool = sut.canExecute() 34 | 35 | // Assert 36 | XCTAssertEqual(expected, actual) 37 | } 38 | 39 | func test_execute_does_not_relay_if_cannot_execute() { 40 | // Arrange 41 | var monitor: Int = 0 42 | let sut = RelayCommand(execute: { Void in monitor = monitor + 1 }, canExecute: { Void in false }) 43 | 44 | // Act 45 | sut.execute() 46 | 47 | // Assert 48 | XCTAssertEqual(0, monitor) 49 | } 50 | 51 | func test_raiseCanExecuteChanged_raises_canExecuteChanged_event() { 52 | // Arrange 53 | let sut = RelayCommand(execute: { _ in }) 54 | var events = [Void]() 55 | _ = sut.canExecuteChanged.subscribe(onNext: { event in events.append(event) }) 56 | 57 | // Act 58 | sut.raiseCanExecuteChanged() 59 | 60 | // Assert 61 | XCTAssertEqual(1, events.count) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /TodoListTests/StubMessageBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StubMessageBox.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/19/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import TodoList 11 | 12 | class StubMessageBox: MessageBox { 13 | 14 | private let _confirm: Bool 15 | 16 | init() { 17 | _confirm = true 18 | } 19 | 20 | init(confirm: Bool) { 21 | _confirm = confirm 22 | } 23 | 24 | func confirm(title: String, message: String, confirmText: String, cancelText: String, destructive: Bool, confirmed: @escaping (Void) -> Void) { 25 | if _confirm { 26 | confirmed() 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /TodoListTests/TodoItemListViewModel_features.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoItemListViewModel_features.swift 3 | // TodoList 4 | // 5 | // Created by Gyuwon Yi on 3/19/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TodoList 11 | 12 | class TodoItemListViewModel_features: XCTestCase { 13 | 14 | func test_deleteItem_command_deletes_item_correctly() { 15 | // Arrange 16 | var data = [ 17 | TodoItemViewModel(description: UUID().uuidString), 18 | TodoItemViewModel(description: UUID().uuidString), 19 | TodoItemViewModel(description: UUID().uuidString) 20 | ] 21 | let sut = TodoItemListViewModel(messageBox: StubMessageBox()) 22 | for i in 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TodoListUITests/TodoListUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoListUITests.swift 3 | // TodoListUITests 4 | // 5 | // Created by Gyuwon Yi on 3/11/17. 6 | // Copyright © 2017 Gyuwon. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class TodoListUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | --------------------------------------------------------------------------------