├── MMA.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── ces.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MMA ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Coordinator │ ├── AppCoordinator.swift │ ├── Coordinator.swift │ └── CoordinatorScreens │ │ └── CoordinatorScreens.swift ├── Factories │ ├── AppFactory.swift │ └── HomeFactory.swift ├── Home │ ├── Data │ │ ├── Local │ │ │ └── TodosDataSourceLocal.swift │ │ ├── Remote │ │ │ └── TodosDataSourceRemote.swift │ │ └── Repository │ │ │ └── GetTodosRepository.swift │ ├── Domain │ │ ├── Entities │ │ │ ├── Errors │ │ │ │ ├── GetTodoError.swift │ │ │ │ └── UpdateTodoError.swift │ │ │ └── Todo.swift │ │ └── Use Cases │ │ │ ├── Data Sources │ │ │ └── GetTodosSource.swift │ │ │ └── GetTodosUseCase.swift │ ├── Framework │ │ ├── Local │ │ │ ├── TodosDb.swift │ │ │ ├── TodosDbDataGateway.swift │ │ │ └── TodosDbImp.swift │ │ ├── Remote │ │ │ ├── TodosRemoteDataGateway.swift │ │ │ ├── TodosService.swift │ │ │ └── TodosServiceImp.swift │ │ └── Views │ │ │ └── HomeView.swift │ └── Presentation │ │ └── HomeViewModel.swift ├── Info.plist └── SceneDelegate.swift ├── MMATests ├── Data │ └── GetTodosRepositoryTests.swift ├── Domain │ └── GetTodosUseCaseTests.swift ├── Framework │ ├── Local │ │ ├── TodosDbDataGatewayTests.swift │ │ └── TodosDbImpTests.swift │ └── Remote │ │ ├── TodosRemoteDataGatewayTests.swift │ │ └── TodosServiceImpTests.swift ├── Helpers │ ├── URLProtocolStub.swift │ └── XCTestCase+Extensions.swift └── Presentation │ └── HomeViewModelTests.swift └── README.md /MMA.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CA4501B829EE61BD00D37E9A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501B729EE61BD00D37E9A /* AppDelegate.swift */; }; 11 | CA4501BA29EE61BD00D37E9A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501B929EE61BD00D37E9A /* SceneDelegate.swift */; }; 12 | CA4501C129EE61BF00D37E9A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA4501C029EE61BF00D37E9A /* Assets.xcassets */; }; 13 | CA4501C429EE61BF00D37E9A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CA4501C229EE61BF00D37E9A /* LaunchScreen.storyboard */; }; 14 | CA4501CF29EE626C00D37E9A /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501CE29EE626C00D37E9A /* Todo.swift */; }; 15 | CA4501D129EE631600D37E9A /* GetTodosUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501D029EE631600D37E9A /* GetTodosUseCase.swift */; }; 16 | CA4501D429EE652000D37E9A /* GetTodosSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501D329EE652000D37E9A /* GetTodosSource.swift */; }; 17 | CA4501D729EE66AF00D37E9A /* GetTodoError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501D629EE66AF00D37E9A /* GetTodoError.swift */; }; 18 | CA4501DF29EE695000D37E9A /* GetTodosUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501DE29EE695000D37E9A /* GetTodosUseCaseTests.swift */; }; 19 | CA4501E829EE6B8300D37E9A /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501E729EE6B8300D37E9A /* XCTestCase+Extensions.swift */; }; 20 | CA4501EE29EE71C100D37E9A /* TodosDataSourceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501ED29EE71C100D37E9A /* TodosDataSourceRemote.swift */; }; 21 | CA4501F029EE727100D37E9A /* TodosDataSourceLocal.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501EF29EE727100D37E9A /* TodosDataSourceLocal.swift */; }; 22 | CA4501F229EE733600D37E9A /* GetTodosRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4501F129EE733600D37E9A /* GetTodosRepository.swift */; }; 23 | CAAB8B5129EEFCBB003D4399 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B5029EEFCBB003D4399 /* HomeViewModel.swift */; }; 24 | CAAB8B5429EF00CA003D4399 /* HomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B5329EF00CA003D4399 /* HomeViewModelTests.swift */; }; 25 | CAAB8B5A29EF1387003D4399 /* TodosService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B5929EF1387003D4399 /* TodosService.swift */; }; 26 | CAAB8B5C29EF1528003D4399 /* TodosServiceImp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B5B29EF1528003D4399 /* TodosServiceImp.swift */; }; 27 | CAAB8B5E29EF1A24003D4399 /* TodosRemoteDataGateway.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B5D29EF1A24003D4399 /* TodosRemoteDataGateway.swift */; }; 28 | CAAB8B6029EF1BD7003D4399 /* TodosDb.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B5F29EF1BD7003D4399 /* TodosDb.swift */; }; 29 | CAAB8B6229EF1CA0003D4399 /* UpdateTodoError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B6129EF1CA0003D4399 /* UpdateTodoError.swift */; }; 30 | CAAB8B6429EF1F9E003D4399 /* TodosDbImp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B6329EF1F9E003D4399 /* TodosDbImp.swift */; }; 31 | CAAB8B6629EF2114003D4399 /* TodosDbDataGateway.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B6529EF2114003D4399 /* TodosDbDataGateway.swift */; }; 32 | CAAB8B6829EF2727003D4399 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B6729EF2727003D4399 /* HomeView.swift */; }; 33 | CAAB8B6D29EF34E8003D4399 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B6C29EF34E8003D4399 /* Coordinator.swift */; }; 34 | CAAB8B6F29EF3583003D4399 /* AppFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B6E29EF3583003D4399 /* AppFactory.swift */; }; 35 | CAAB8B7129EF35BD003D4399 /* HomeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B7029EF35BD003D4399 /* HomeFactory.swift */; }; 36 | CAAB8B7329EF363B003D4399 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B7229EF363B003D4399 /* AppCoordinator.swift */; }; 37 | CAAB8B7629EF37B8003D4399 /* CoordinatorScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAB8B7529EF37B8003D4399 /* CoordinatorScreens.swift */; }; 38 | CAAEBC4729EF6D9500C0D4A4 /* TodosServiceImpTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAEBC4629EF6D9500C0D4A4 /* TodosServiceImpTests.swift */; }; 39 | CAB35D5329EF75B500F4CD47 /* URLProtocolStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB35D5229EF75B500F4CD47 /* URLProtocolStub.swift */; }; 40 | CAB35D5629EF83A800F4CD47 /* TodosDbImpTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB35D5529EF83A800F4CD47 /* TodosDbImpTests.swift */; }; 41 | CAB4F26C29EEE73400E426D7 /* GetTodosRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB4F26B29EEE73400E426D7 /* GetTodosRepositoryTests.swift */; }; 42 | CAB61AC029F03EDC00A2EE63 /* TodosDbDataGatewayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB61ABF29F03EDC00A2EE63 /* TodosDbDataGatewayTests.swift */; }; 43 | CAB61AC229F0484900A2EE63 /* TodosRemoteDataGatewayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB61AC129F0484900A2EE63 /* TodosRemoteDataGatewayTests.swift */; }; 44 | /* End PBXBuildFile section */ 45 | 46 | /* Begin PBXContainerItemProxy section */ 47 | CA4501E029EE695000D37E9A /* PBXContainerItemProxy */ = { 48 | isa = PBXContainerItemProxy; 49 | containerPortal = CA4501AC29EE61BD00D37E9A /* Project object */; 50 | proxyType = 1; 51 | remoteGlobalIDString = CA4501B329EE61BD00D37E9A; 52 | remoteInfo = MMA; 53 | }; 54 | /* End PBXContainerItemProxy section */ 55 | 56 | /* Begin PBXFileReference section */ 57 | CA4501B429EE61BD00D37E9A /* MMA.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MMA.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | CA4501B729EE61BD00D37E9A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | CA4501B929EE61BD00D37E9A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 60 | CA4501C029EE61BF00D37E9A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61 | CA4501C329EE61BF00D37E9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 62 | CA4501C529EE61BF00D37E9A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63 | CA4501CE29EE626C00D37E9A /* Todo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = ""; }; 64 | CA4501D029EE631600D37E9A /* GetTodosUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTodosUseCase.swift; sourceTree = ""; }; 65 | CA4501D329EE652000D37E9A /* GetTodosSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTodosSource.swift; sourceTree = ""; }; 66 | CA4501D629EE66AF00D37E9A /* GetTodoError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTodoError.swift; sourceTree = ""; }; 67 | CA4501DC29EE695000D37E9A /* MMATests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MMATests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | CA4501DE29EE695000D37E9A /* GetTodosUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTodosUseCaseTests.swift; sourceTree = ""; }; 69 | CA4501E729EE6B8300D37E9A /* XCTestCase+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Extensions.swift"; sourceTree = ""; }; 70 | CA4501ED29EE71C100D37E9A /* TodosDataSourceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosDataSourceRemote.swift; sourceTree = ""; }; 71 | CA4501EF29EE727100D37E9A /* TodosDataSourceLocal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosDataSourceLocal.swift; sourceTree = ""; }; 72 | CA4501F129EE733600D37E9A /* GetTodosRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTodosRepository.swift; sourceTree = ""; }; 73 | CAAB8B5029EEFCBB003D4399 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 74 | CAAB8B5329EF00CA003D4399 /* HomeViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModelTests.swift; sourceTree = ""; }; 75 | CAAB8B5929EF1387003D4399 /* TodosService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosService.swift; sourceTree = ""; }; 76 | CAAB8B5B29EF1528003D4399 /* TodosServiceImp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosServiceImp.swift; sourceTree = ""; }; 77 | CAAB8B5D29EF1A24003D4399 /* TodosRemoteDataGateway.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosRemoteDataGateway.swift; sourceTree = ""; }; 78 | CAAB8B5F29EF1BD7003D4399 /* TodosDb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosDb.swift; sourceTree = ""; }; 79 | CAAB8B6129EF1CA0003D4399 /* UpdateTodoError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTodoError.swift; sourceTree = ""; }; 80 | CAAB8B6329EF1F9E003D4399 /* TodosDbImp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosDbImp.swift; sourceTree = ""; }; 81 | CAAB8B6529EF2114003D4399 /* TodosDbDataGateway.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosDbDataGateway.swift; sourceTree = ""; }; 82 | CAAB8B6729EF2727003D4399 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 83 | CAAB8B6C29EF34E8003D4399 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 84 | CAAB8B6E29EF3583003D4399 /* AppFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFactory.swift; sourceTree = ""; }; 85 | CAAB8B7029EF35BD003D4399 /* HomeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFactory.swift; sourceTree = ""; }; 86 | CAAB8B7229EF363B003D4399 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 87 | CAAB8B7529EF37B8003D4399 /* CoordinatorScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorScreens.swift; sourceTree = ""; }; 88 | CAAEBC4629EF6D9500C0D4A4 /* TodosServiceImpTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosServiceImpTests.swift; sourceTree = ""; }; 89 | CAB35D5229EF75B500F4CD47 /* URLProtocolStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLProtocolStub.swift; sourceTree = ""; }; 90 | CAB35D5529EF83A800F4CD47 /* TodosDbImpTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosDbImpTests.swift; sourceTree = ""; }; 91 | CAB4F26B29EEE73400E426D7 /* GetTodosRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTodosRepositoryTests.swift; sourceTree = ""; }; 92 | CAB61ABF29F03EDC00A2EE63 /* TodosDbDataGatewayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosDbDataGatewayTests.swift; sourceTree = ""; }; 93 | CAB61AC129F0484900A2EE63 /* TodosRemoteDataGatewayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosRemoteDataGatewayTests.swift; sourceTree = ""; }; 94 | /* End PBXFileReference section */ 95 | 96 | /* Begin PBXFrameworksBuildPhase section */ 97 | CA4501B129EE61BD00D37E9A /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | CA4501D929EE695000D37E9A /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | /* End PBXFrameworksBuildPhase section */ 112 | 113 | /* Begin PBXGroup section */ 114 | CA4501AB29EE61BD00D37E9A = { 115 | isa = PBXGroup; 116 | children = ( 117 | CA4501B629EE61BD00D37E9A /* MMA */, 118 | CA4501DD29EE695000D37E9A /* MMATests */, 119 | CA4501B529EE61BD00D37E9A /* Products */, 120 | ); 121 | sourceTree = ""; 122 | }; 123 | CA4501B529EE61BD00D37E9A /* Products */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | CA4501B429EE61BD00D37E9A /* MMA.app */, 127 | CA4501DC29EE695000D37E9A /* MMATests.xctest */, 128 | ); 129 | name = Products; 130 | sourceTree = ""; 131 | }; 132 | CA4501B629EE61BD00D37E9A /* MMA */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | CAAB8B6B29EF34D2003D4399 /* Factories */, 136 | CAAB8B6A29EF34CD003D4399 /* Coordinator */, 137 | CAAB8B6929EF34B3003D4399 /* Home */, 138 | CA4501B729EE61BD00D37E9A /* AppDelegate.swift */, 139 | CA4501B929EE61BD00D37E9A /* SceneDelegate.swift */, 140 | CA4501C029EE61BF00D37E9A /* Assets.xcassets */, 141 | CA4501C229EE61BF00D37E9A /* LaunchScreen.storyboard */, 142 | CA4501C529EE61BF00D37E9A /* Info.plist */, 143 | ); 144 | path = MMA; 145 | sourceTree = ""; 146 | }; 147 | CA4501CB29EE620C00D37E9A /* Domain */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | CA4501CD29EE621900D37E9A /* Use Cases */, 151 | CA4501CC29EE621300D37E9A /* Entities */, 152 | ); 153 | path = Domain; 154 | sourceTree = ""; 155 | }; 156 | CA4501CC29EE621300D37E9A /* Entities */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | CA4501CE29EE626C00D37E9A /* Todo.swift */, 160 | CA4501D529EE66A600D37E9A /* Errors */, 161 | ); 162 | path = Entities; 163 | sourceTree = ""; 164 | }; 165 | CA4501CD29EE621900D37E9A /* Use Cases */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | CA4501D029EE631600D37E9A /* GetTodosUseCase.swift */, 169 | CA4501D229EE64F200D37E9A /* Data Sources */, 170 | ); 171 | path = "Use Cases"; 172 | sourceTree = ""; 173 | }; 174 | CA4501D229EE64F200D37E9A /* Data Sources */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | CA4501D329EE652000D37E9A /* GetTodosSource.swift */, 178 | ); 179 | path = "Data Sources"; 180 | sourceTree = ""; 181 | }; 182 | CA4501D529EE66A600D37E9A /* Errors */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | CA4501D629EE66AF00D37E9A /* GetTodoError.swift */, 186 | CAAB8B6129EF1CA0003D4399 /* UpdateTodoError.swift */, 187 | ); 188 | path = Errors; 189 | sourceTree = ""; 190 | }; 191 | CA4501DD29EE695000D37E9A /* MMATests */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | CAAEBC4429EF6D8400C0D4A4 /* Framework */, 195 | CAAB8B5229EF008D003D4399 /* Presentation */, 196 | CAB4F26629EEE6E500E426D7 /* Data */, 197 | CA4501E629EE6B5A00D37E9A /* Helpers */, 198 | CA4501E529EE695C00D37E9A /* Domain */, 199 | ); 200 | path = MMATests; 201 | sourceTree = ""; 202 | }; 203 | CA4501E529EE695C00D37E9A /* Domain */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | CA4501DE29EE695000D37E9A /* GetTodosUseCaseTests.swift */, 207 | ); 208 | path = Domain; 209 | sourceTree = ""; 210 | }; 211 | CA4501E629EE6B5A00D37E9A /* Helpers */ = { 212 | isa = PBXGroup; 213 | children = ( 214 | CA4501E729EE6B8300D37E9A /* XCTestCase+Extensions.swift */, 215 | CAB35D5229EF75B500F4CD47 /* URLProtocolStub.swift */, 216 | ); 217 | path = Helpers; 218 | sourceTree = ""; 219 | }; 220 | CA4501E929EE715900D37E9A /* Data */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | CA4501EC29EE716A00D37E9A /* Repository */, 224 | CA4501EB29EE716000D37E9A /* Remote */, 225 | CA4501EA29EE715D00D37E9A /* Local */, 226 | ); 227 | path = Data; 228 | sourceTree = ""; 229 | }; 230 | CA4501EA29EE715D00D37E9A /* Local */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | CA4501EF29EE727100D37E9A /* TodosDataSourceLocal.swift */, 234 | ); 235 | path = Local; 236 | sourceTree = ""; 237 | }; 238 | CA4501EB29EE716000D37E9A /* Remote */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | CA4501ED29EE71C100D37E9A /* TodosDataSourceRemote.swift */, 242 | ); 243 | path = Remote; 244 | sourceTree = ""; 245 | }; 246 | CA4501EC29EE716A00D37E9A /* Repository */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | CA4501F129EE733600D37E9A /* GetTodosRepository.swift */, 250 | ); 251 | path = Repository; 252 | sourceTree = ""; 253 | }; 254 | CAAB8B4F29EEFC89003D4399 /* Presentation */ = { 255 | isa = PBXGroup; 256 | children = ( 257 | CAAB8B5029EEFCBB003D4399 /* HomeViewModel.swift */, 258 | ); 259 | path = Presentation; 260 | sourceTree = ""; 261 | }; 262 | CAAB8B5229EF008D003D4399 /* Presentation */ = { 263 | isa = PBXGroup; 264 | children = ( 265 | CAAB8B5329EF00CA003D4399 /* HomeViewModelTests.swift */, 266 | ); 267 | path = Presentation; 268 | sourceTree = ""; 269 | }; 270 | CAAB8B5529EF0EF1003D4399 /* Framework */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | CAAB8B5829EF0F13003D4399 /* Views */, 274 | CAAB8B5729EF0F0F003D4399 /* Local */, 275 | CAAB8B5629EF0F0A003D4399 /* Remote */, 276 | ); 277 | path = Framework; 278 | sourceTree = ""; 279 | }; 280 | CAAB8B5629EF0F0A003D4399 /* Remote */ = { 281 | isa = PBXGroup; 282 | children = ( 283 | CAAB8B5929EF1387003D4399 /* TodosService.swift */, 284 | CAAB8B5D29EF1A24003D4399 /* TodosRemoteDataGateway.swift */, 285 | CAAB8B5B29EF1528003D4399 /* TodosServiceImp.swift */, 286 | ); 287 | path = Remote; 288 | sourceTree = ""; 289 | }; 290 | CAAB8B5729EF0F0F003D4399 /* Local */ = { 291 | isa = PBXGroup; 292 | children = ( 293 | CAAB8B5F29EF1BD7003D4399 /* TodosDb.swift */, 294 | CAAB8B6329EF1F9E003D4399 /* TodosDbImp.swift */, 295 | CAAB8B6529EF2114003D4399 /* TodosDbDataGateway.swift */, 296 | ); 297 | path = Local; 298 | sourceTree = ""; 299 | }; 300 | CAAB8B5829EF0F13003D4399 /* Views */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | CAAB8B6729EF2727003D4399 /* HomeView.swift */, 304 | ); 305 | path = Views; 306 | sourceTree = ""; 307 | }; 308 | CAAB8B6929EF34B3003D4399 /* Home */ = { 309 | isa = PBXGroup; 310 | children = ( 311 | CAAB8B5529EF0EF1003D4399 /* Framework */, 312 | CAAB8B4F29EEFC89003D4399 /* Presentation */, 313 | CA4501E929EE715900D37E9A /* Data */, 314 | CA4501CB29EE620C00D37E9A /* Domain */, 315 | ); 316 | path = Home; 317 | sourceTree = ""; 318 | }; 319 | CAAB8B6A29EF34CD003D4399 /* Coordinator */ = { 320 | isa = PBXGroup; 321 | children = ( 322 | CAAB8B7429EF37A8003D4399 /* CoordinatorScreens */, 323 | CAAB8B6C29EF34E8003D4399 /* Coordinator.swift */, 324 | CAAB8B7229EF363B003D4399 /* AppCoordinator.swift */, 325 | ); 326 | path = Coordinator; 327 | sourceTree = ""; 328 | }; 329 | CAAB8B6B29EF34D2003D4399 /* Factories */ = { 330 | isa = PBXGroup; 331 | children = ( 332 | CAAB8B6E29EF3583003D4399 /* AppFactory.swift */, 333 | CAAB8B7029EF35BD003D4399 /* HomeFactory.swift */, 334 | ); 335 | path = Factories; 336 | sourceTree = ""; 337 | }; 338 | CAAB8B7429EF37A8003D4399 /* CoordinatorScreens */ = { 339 | isa = PBXGroup; 340 | children = ( 341 | CAAB8B7529EF37B8003D4399 /* CoordinatorScreens.swift */, 342 | ); 343 | path = CoordinatorScreens; 344 | sourceTree = ""; 345 | }; 346 | CAAEBC4429EF6D8400C0D4A4 /* Framework */ = { 347 | isa = PBXGroup; 348 | children = ( 349 | CAB35D5429EF839C00F4CD47 /* Local */, 350 | CAAEBC4529EF6D8800C0D4A4 /* Remote */, 351 | ); 352 | path = Framework; 353 | sourceTree = ""; 354 | }; 355 | CAAEBC4529EF6D8800C0D4A4 /* Remote */ = { 356 | isa = PBXGroup; 357 | children = ( 358 | CAAEBC4629EF6D9500C0D4A4 /* TodosServiceImpTests.swift */, 359 | CAB61AC129F0484900A2EE63 /* TodosRemoteDataGatewayTests.swift */, 360 | ); 361 | path = Remote; 362 | sourceTree = ""; 363 | }; 364 | CAB35D5429EF839C00F4CD47 /* Local */ = { 365 | isa = PBXGroup; 366 | children = ( 367 | CAB35D5529EF83A800F4CD47 /* TodosDbImpTests.swift */, 368 | CAB61ABF29F03EDC00A2EE63 /* TodosDbDataGatewayTests.swift */, 369 | ); 370 | path = Local; 371 | sourceTree = ""; 372 | }; 373 | CAB4F26629EEE6E500E426D7 /* Data */ = { 374 | isa = PBXGroup; 375 | children = ( 376 | CAB4F26B29EEE73400E426D7 /* GetTodosRepositoryTests.swift */, 377 | ); 378 | path = Data; 379 | sourceTree = ""; 380 | }; 381 | /* End PBXGroup section */ 382 | 383 | /* Begin PBXNativeTarget section */ 384 | CA4501B329EE61BD00D37E9A /* MMA */ = { 385 | isa = PBXNativeTarget; 386 | buildConfigurationList = CA4501C829EE61BF00D37E9A /* Build configuration list for PBXNativeTarget "MMA" */; 387 | buildPhases = ( 388 | CA4501B029EE61BD00D37E9A /* Sources */, 389 | CA4501B129EE61BD00D37E9A /* Frameworks */, 390 | CA4501B229EE61BD00D37E9A /* Resources */, 391 | ); 392 | buildRules = ( 393 | ); 394 | dependencies = ( 395 | ); 396 | name = MMA; 397 | productName = MMA; 398 | productReference = CA4501B429EE61BD00D37E9A /* MMA.app */; 399 | productType = "com.apple.product-type.application"; 400 | }; 401 | CA4501DB29EE695000D37E9A /* MMATests */ = { 402 | isa = PBXNativeTarget; 403 | buildConfigurationList = CA4501E229EE695000D37E9A /* Build configuration list for PBXNativeTarget "MMATests" */; 404 | buildPhases = ( 405 | CA4501D829EE695000D37E9A /* Sources */, 406 | CA4501D929EE695000D37E9A /* Frameworks */, 407 | CA4501DA29EE695000D37E9A /* Resources */, 408 | ); 409 | buildRules = ( 410 | ); 411 | dependencies = ( 412 | CA4501E129EE695000D37E9A /* PBXTargetDependency */, 413 | ); 414 | name = MMATests; 415 | productName = MMATests; 416 | productReference = CA4501DC29EE695000D37E9A /* MMATests.xctest */; 417 | productType = "com.apple.product-type.bundle.unit-test"; 418 | }; 419 | /* End PBXNativeTarget section */ 420 | 421 | /* Begin PBXProject section */ 422 | CA4501AC29EE61BD00D37E9A /* Project object */ = { 423 | isa = PBXProject; 424 | attributes = { 425 | BuildIndependentTargetsInParallel = 1; 426 | LastSwiftUpdateCheck = 1430; 427 | LastUpgradeCheck = 1430; 428 | TargetAttributes = { 429 | CA4501B329EE61BD00D37E9A = { 430 | CreatedOnToolsVersion = 14.3; 431 | }; 432 | CA4501DB29EE695000D37E9A = { 433 | CreatedOnToolsVersion = 14.3; 434 | TestTargetID = CA4501B329EE61BD00D37E9A; 435 | }; 436 | }; 437 | }; 438 | buildConfigurationList = CA4501AF29EE61BD00D37E9A /* Build configuration list for PBXProject "MMA" */; 439 | compatibilityVersion = "Xcode 14.0"; 440 | developmentRegion = en; 441 | hasScannedForEncodings = 0; 442 | knownRegions = ( 443 | en, 444 | Base, 445 | ); 446 | mainGroup = CA4501AB29EE61BD00D37E9A; 447 | productRefGroup = CA4501B529EE61BD00D37E9A /* Products */; 448 | projectDirPath = ""; 449 | projectRoot = ""; 450 | targets = ( 451 | CA4501B329EE61BD00D37E9A /* MMA */, 452 | CA4501DB29EE695000D37E9A /* MMATests */, 453 | ); 454 | }; 455 | /* End PBXProject section */ 456 | 457 | /* Begin PBXResourcesBuildPhase section */ 458 | CA4501B229EE61BD00D37E9A /* Resources */ = { 459 | isa = PBXResourcesBuildPhase; 460 | buildActionMask = 2147483647; 461 | files = ( 462 | CA4501C429EE61BF00D37E9A /* LaunchScreen.storyboard in Resources */, 463 | CA4501C129EE61BF00D37E9A /* Assets.xcassets in Resources */, 464 | ); 465 | runOnlyForDeploymentPostprocessing = 0; 466 | }; 467 | CA4501DA29EE695000D37E9A /* Resources */ = { 468 | isa = PBXResourcesBuildPhase; 469 | buildActionMask = 2147483647; 470 | files = ( 471 | ); 472 | runOnlyForDeploymentPostprocessing = 0; 473 | }; 474 | /* End PBXResourcesBuildPhase section */ 475 | 476 | /* Begin PBXSourcesBuildPhase section */ 477 | CA4501B029EE61BD00D37E9A /* Sources */ = { 478 | isa = PBXSourcesBuildPhase; 479 | buildActionMask = 2147483647; 480 | files = ( 481 | CA4501D729EE66AF00D37E9A /* GetTodoError.swift in Sources */, 482 | CAAB8B7129EF35BD003D4399 /* HomeFactory.swift in Sources */, 483 | CAAB8B5129EEFCBB003D4399 /* HomeViewModel.swift in Sources */, 484 | CAAB8B6229EF1CA0003D4399 /* UpdateTodoError.swift in Sources */, 485 | CAAB8B6629EF2114003D4399 /* TodosDbDataGateway.swift in Sources */, 486 | CAAB8B7329EF363B003D4399 /* AppCoordinator.swift in Sources */, 487 | CAAB8B5A29EF1387003D4399 /* TodosService.swift in Sources */, 488 | CA4501D129EE631600D37E9A /* GetTodosUseCase.swift in Sources */, 489 | CA4501B829EE61BD00D37E9A /* AppDelegate.swift in Sources */, 490 | CAAB8B7629EF37B8003D4399 /* CoordinatorScreens.swift in Sources */, 491 | CAAB8B5E29EF1A24003D4399 /* TodosRemoteDataGateway.swift in Sources */, 492 | CA4501F229EE733600D37E9A /* GetTodosRepository.swift in Sources */, 493 | CAAB8B5C29EF1528003D4399 /* TodosServiceImp.swift in Sources */, 494 | CA4501BA29EE61BD00D37E9A /* SceneDelegate.swift in Sources */, 495 | CAAB8B6F29EF3583003D4399 /* AppFactory.swift in Sources */, 496 | CAAB8B6D29EF34E8003D4399 /* Coordinator.swift in Sources */, 497 | CA4501CF29EE626C00D37E9A /* Todo.swift in Sources */, 498 | CA4501F029EE727100D37E9A /* TodosDataSourceLocal.swift in Sources */, 499 | CA4501EE29EE71C100D37E9A /* TodosDataSourceRemote.swift in Sources */, 500 | CAAB8B6029EF1BD7003D4399 /* TodosDb.swift in Sources */, 501 | CA4501D429EE652000D37E9A /* GetTodosSource.swift in Sources */, 502 | CAAB8B6429EF1F9E003D4399 /* TodosDbImp.swift in Sources */, 503 | CAAB8B6829EF2727003D4399 /* HomeView.swift in Sources */, 504 | ); 505 | runOnlyForDeploymentPostprocessing = 0; 506 | }; 507 | CA4501D829EE695000D37E9A /* Sources */ = { 508 | isa = PBXSourcesBuildPhase; 509 | buildActionMask = 2147483647; 510 | files = ( 511 | CAB61AC229F0484900A2EE63 /* TodosRemoteDataGatewayTests.swift in Sources */, 512 | CAAEBC4729EF6D9500C0D4A4 /* TodosServiceImpTests.swift in Sources */, 513 | CAB35D5329EF75B500F4CD47 /* URLProtocolStub.swift in Sources */, 514 | CA4501E829EE6B8300D37E9A /* XCTestCase+Extensions.swift in Sources */, 515 | CAB4F26C29EEE73400E426D7 /* GetTodosRepositoryTests.swift in Sources */, 516 | CA4501DF29EE695000D37E9A /* GetTodosUseCaseTests.swift in Sources */, 517 | CAAB8B5429EF00CA003D4399 /* HomeViewModelTests.swift in Sources */, 518 | CAB61AC029F03EDC00A2EE63 /* TodosDbDataGatewayTests.swift in Sources */, 519 | CAB35D5629EF83A800F4CD47 /* TodosDbImpTests.swift in Sources */, 520 | ); 521 | runOnlyForDeploymentPostprocessing = 0; 522 | }; 523 | /* End PBXSourcesBuildPhase section */ 524 | 525 | /* Begin PBXTargetDependency section */ 526 | CA4501E129EE695000D37E9A /* PBXTargetDependency */ = { 527 | isa = PBXTargetDependency; 528 | target = CA4501B329EE61BD00D37E9A /* MMA */; 529 | targetProxy = CA4501E029EE695000D37E9A /* PBXContainerItemProxy */; 530 | }; 531 | /* End PBXTargetDependency section */ 532 | 533 | /* Begin PBXVariantGroup section */ 534 | CA4501C229EE61BF00D37E9A /* LaunchScreen.storyboard */ = { 535 | isa = PBXVariantGroup; 536 | children = ( 537 | CA4501C329EE61BF00D37E9A /* Base */, 538 | ); 539 | name = LaunchScreen.storyboard; 540 | sourceTree = ""; 541 | }; 542 | /* End PBXVariantGroup section */ 543 | 544 | /* Begin XCBuildConfiguration section */ 545 | CA4501C629EE61BF00D37E9A /* Debug */ = { 546 | isa = XCBuildConfiguration; 547 | buildSettings = { 548 | ALWAYS_SEARCH_USER_PATHS = NO; 549 | CLANG_ANALYZER_NONNULL = YES; 550 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 551 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 552 | CLANG_ENABLE_MODULES = YES; 553 | CLANG_ENABLE_OBJC_ARC = YES; 554 | CLANG_ENABLE_OBJC_WEAK = YES; 555 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 556 | CLANG_WARN_BOOL_CONVERSION = YES; 557 | CLANG_WARN_COMMA = YES; 558 | CLANG_WARN_CONSTANT_CONVERSION = YES; 559 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 560 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 561 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 562 | CLANG_WARN_EMPTY_BODY = YES; 563 | CLANG_WARN_ENUM_CONVERSION = YES; 564 | CLANG_WARN_INFINITE_RECURSION = YES; 565 | CLANG_WARN_INT_CONVERSION = YES; 566 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 567 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 568 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 569 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 570 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 571 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 572 | CLANG_WARN_STRICT_PROTOTYPES = YES; 573 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 574 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 575 | CLANG_WARN_UNREACHABLE_CODE = YES; 576 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 577 | COPY_PHASE_STRIP = NO; 578 | DEBUG_INFORMATION_FORMAT = dwarf; 579 | ENABLE_STRICT_OBJC_MSGSEND = YES; 580 | ENABLE_TESTABILITY = YES; 581 | GCC_C_LANGUAGE_STANDARD = gnu11; 582 | GCC_DYNAMIC_NO_PIC = NO; 583 | GCC_NO_COMMON_BLOCKS = YES; 584 | GCC_OPTIMIZATION_LEVEL = 0; 585 | GCC_PREPROCESSOR_DEFINITIONS = ( 586 | "DEBUG=1", 587 | "$(inherited)", 588 | ); 589 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 590 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 591 | GCC_WARN_UNDECLARED_SELECTOR = YES; 592 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 593 | GCC_WARN_UNUSED_FUNCTION = YES; 594 | GCC_WARN_UNUSED_VARIABLE = YES; 595 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 596 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 597 | MTL_FAST_MATH = YES; 598 | ONLY_ACTIVE_ARCH = YES; 599 | SDKROOT = iphoneos; 600 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 601 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 602 | }; 603 | name = Debug; 604 | }; 605 | CA4501C729EE61BF00D37E9A /* Release */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | ALWAYS_SEARCH_USER_PATHS = NO; 609 | CLANG_ANALYZER_NONNULL = YES; 610 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 611 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 612 | CLANG_ENABLE_MODULES = YES; 613 | CLANG_ENABLE_OBJC_ARC = YES; 614 | CLANG_ENABLE_OBJC_WEAK = YES; 615 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 616 | CLANG_WARN_BOOL_CONVERSION = YES; 617 | CLANG_WARN_COMMA = YES; 618 | CLANG_WARN_CONSTANT_CONVERSION = YES; 619 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 620 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 621 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 622 | CLANG_WARN_EMPTY_BODY = YES; 623 | CLANG_WARN_ENUM_CONVERSION = YES; 624 | CLANG_WARN_INFINITE_RECURSION = YES; 625 | CLANG_WARN_INT_CONVERSION = YES; 626 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 627 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 628 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 629 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 630 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 631 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 632 | CLANG_WARN_STRICT_PROTOTYPES = YES; 633 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 634 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 635 | CLANG_WARN_UNREACHABLE_CODE = YES; 636 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 637 | COPY_PHASE_STRIP = NO; 638 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 639 | ENABLE_NS_ASSERTIONS = NO; 640 | ENABLE_STRICT_OBJC_MSGSEND = YES; 641 | GCC_C_LANGUAGE_STANDARD = gnu11; 642 | GCC_NO_COMMON_BLOCKS = YES; 643 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 644 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 645 | GCC_WARN_UNDECLARED_SELECTOR = YES; 646 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 647 | GCC_WARN_UNUSED_FUNCTION = YES; 648 | GCC_WARN_UNUSED_VARIABLE = YES; 649 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 650 | MTL_ENABLE_DEBUG_INFO = NO; 651 | MTL_FAST_MATH = YES; 652 | SDKROOT = iphoneos; 653 | SWIFT_COMPILATION_MODE = wholemodule; 654 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 655 | VALIDATE_PRODUCT = YES; 656 | }; 657 | name = Release; 658 | }; 659 | CA4501C929EE61BF00D37E9A /* Debug */ = { 660 | isa = XCBuildConfiguration; 661 | buildSettings = { 662 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 663 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 664 | CODE_SIGN_STYLE = Automatic; 665 | CURRENT_PROJECT_VERSION = 1; 666 | DEVELOPMENT_TEAM = FVH6V77N95; 667 | GENERATE_INFOPLIST_FILE = YES; 668 | INFOPLIST_FILE = MMA/Info.plist; 669 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 670 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 671 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 672 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 673 | LD_RUNPATH_SEARCH_PATHS = ( 674 | "$(inherited)", 675 | "@executable_path/Frameworks", 676 | ); 677 | MARKETING_VERSION = 1.0; 678 | PRODUCT_BUNDLE_IDENTIFIER = cesmejia.MMA; 679 | PRODUCT_NAME = "$(TARGET_NAME)"; 680 | SWIFT_EMIT_LOC_STRINGS = YES; 681 | SWIFT_VERSION = 5.0; 682 | TARGETED_DEVICE_FAMILY = "1,2"; 683 | }; 684 | name = Debug; 685 | }; 686 | CA4501CA29EE61BF00D37E9A /* Release */ = { 687 | isa = XCBuildConfiguration; 688 | buildSettings = { 689 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 690 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 691 | CODE_SIGN_STYLE = Automatic; 692 | CURRENT_PROJECT_VERSION = 1; 693 | DEVELOPMENT_TEAM = FVH6V77N95; 694 | GENERATE_INFOPLIST_FILE = YES; 695 | INFOPLIST_FILE = MMA/Info.plist; 696 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 697 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 698 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 699 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 700 | LD_RUNPATH_SEARCH_PATHS = ( 701 | "$(inherited)", 702 | "@executable_path/Frameworks", 703 | ); 704 | MARKETING_VERSION = 1.0; 705 | PRODUCT_BUNDLE_IDENTIFIER = cesmejia.MMA; 706 | PRODUCT_NAME = "$(TARGET_NAME)"; 707 | SWIFT_EMIT_LOC_STRINGS = YES; 708 | SWIFT_VERSION = 5.0; 709 | TARGETED_DEVICE_FAMILY = "1,2"; 710 | }; 711 | name = Release; 712 | }; 713 | CA4501E329EE695000D37E9A /* Debug */ = { 714 | isa = XCBuildConfiguration; 715 | buildSettings = { 716 | BUNDLE_LOADER = "$(TEST_HOST)"; 717 | CODE_SIGN_STYLE = Automatic; 718 | CURRENT_PROJECT_VERSION = 1; 719 | DEVELOPMENT_TEAM = FVH6V77N95; 720 | GENERATE_INFOPLIST_FILE = YES; 721 | MARKETING_VERSION = 1.0; 722 | PRODUCT_BUNDLE_IDENTIFIER = cesmejia.MMATests; 723 | PRODUCT_NAME = "$(TARGET_NAME)"; 724 | SWIFT_EMIT_LOC_STRINGS = NO; 725 | SWIFT_VERSION = 5.0; 726 | TARGETED_DEVICE_FAMILY = "1,2"; 727 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MMA.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MMA"; 728 | }; 729 | name = Debug; 730 | }; 731 | CA4501E429EE695000D37E9A /* Release */ = { 732 | isa = XCBuildConfiguration; 733 | buildSettings = { 734 | BUNDLE_LOADER = "$(TEST_HOST)"; 735 | CODE_SIGN_STYLE = Automatic; 736 | CURRENT_PROJECT_VERSION = 1; 737 | DEVELOPMENT_TEAM = FVH6V77N95; 738 | GENERATE_INFOPLIST_FILE = YES; 739 | MARKETING_VERSION = 1.0; 740 | PRODUCT_BUNDLE_IDENTIFIER = cesmejia.MMATests; 741 | PRODUCT_NAME = "$(TARGET_NAME)"; 742 | SWIFT_EMIT_LOC_STRINGS = NO; 743 | SWIFT_VERSION = 5.0; 744 | TARGETED_DEVICE_FAMILY = "1,2"; 745 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MMA.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MMA"; 746 | }; 747 | name = Release; 748 | }; 749 | /* End XCBuildConfiguration section */ 750 | 751 | /* Begin XCConfigurationList section */ 752 | CA4501AF29EE61BD00D37E9A /* Build configuration list for PBXProject "MMA" */ = { 753 | isa = XCConfigurationList; 754 | buildConfigurations = ( 755 | CA4501C629EE61BF00D37E9A /* Debug */, 756 | CA4501C729EE61BF00D37E9A /* Release */, 757 | ); 758 | defaultConfigurationIsVisible = 0; 759 | defaultConfigurationName = Release; 760 | }; 761 | CA4501C829EE61BF00D37E9A /* Build configuration list for PBXNativeTarget "MMA" */ = { 762 | isa = XCConfigurationList; 763 | buildConfigurations = ( 764 | CA4501C929EE61BF00D37E9A /* Debug */, 765 | CA4501CA29EE61BF00D37E9A /* Release */, 766 | ); 767 | defaultConfigurationIsVisible = 0; 768 | defaultConfigurationName = Release; 769 | }; 770 | CA4501E229EE695000D37E9A /* Build configuration list for PBXNativeTarget "MMATests" */ = { 771 | isa = XCConfigurationList; 772 | buildConfigurations = ( 773 | CA4501E329EE695000D37E9A /* Debug */, 774 | CA4501E429EE695000D37E9A /* Release */, 775 | ); 776 | defaultConfigurationIsVisible = 0; 777 | defaultConfigurationName = Release; 778 | }; 779 | /* End XCConfigurationList section */ 780 | }; 781 | rootObject = CA4501AC29EE61BD00D37E9A /* Project object */; 782 | } 783 | -------------------------------------------------------------------------------- /MMA.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MMA.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MMA.xcodeproj/xcuserdata/ces.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MMA.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MMA/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/17/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /MMA/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MMA/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MMA/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MMA/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 | -------------------------------------------------------------------------------- /MMA/Coordinator/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AppCoordinator: Coordinator { 11 | var navigation: UINavigationController 12 | private let appFactory: AppFactory 13 | 14 | init(navigation: UINavigationController, appFactory: AppFactory, window: UIWindow?) { 15 | self.navigation = navigation 16 | self.appFactory = appFactory 17 | configWindow(window: window) 18 | } 19 | 20 | func start() { 21 | let coordinator = appFactory.makeHomeCoordinator(navigation: navigation) 22 | coordinator.start() 23 | } 24 | 25 | private func configWindow(window: UIWindow?) { 26 | window?.rootViewController = navigation 27 | window?.makeKeyAndVisible() 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /MMA/Coordinator/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Coordinator { 11 | var navigation: UINavigationController { get } 12 | func start() 13 | } 14 | -------------------------------------------------------------------------------- /MMA/Coordinator/CoordinatorScreens/CoordinatorScreens.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorScreens.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class HomeCoordinator: Coordinator { 11 | var navigation: UINavigationController 12 | private let homeFactory: HomeFactory 13 | 14 | init(navigation: UINavigationController, homeFactory: HomeFactory) { 15 | self.navigation = navigation 16 | self.homeFactory = homeFactory 17 | } 18 | 19 | func start() { 20 | let controller = homeFactory.makeModule() 21 | navigation.navigationBar.prefersLargeTitles = true 22 | navigation.pushViewController(controller, animated: true) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /MMA/Factories/AppFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppFactory.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol AppFactory { 11 | func makeHomeCoordinator(navigation: UINavigationController) -> Coordinator 12 | } 13 | 14 | struct AppFactoryImp: AppFactory { 15 | func makeHomeCoordinator(navigation: UINavigationController) -> Coordinator { 16 | let homeFactory = HomeFactoryImp() 17 | let homeCoordinator = HomeCoordinator(navigation: navigation, homeFactory: homeFactory) 18 | return homeCoordinator 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MMA/Factories/HomeFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeFactory.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | protocol HomeFactory { 11 | func makeModule() -> UIViewController 12 | } 13 | 14 | struct HomeFactoryImp: HomeFactory { 15 | @MainActor 16 | func makeModule() -> UIViewController { 17 | let todosService = TodosServiceImp() 18 | let todosDb = TodosDbImp() 19 | let todosDataSourceRemote = TodosRemoteDataGateway(service: todosService, db: todosDb) 20 | let todosDataSourceLocal = TodosDbDataGateway(db: todosDb) 21 | let getTodosSource = GetTodosRepository(todosRemoteSource: todosDataSourceRemote, todosLocalSource: todosDataSourceLocal) 22 | let getTodosUseCase = GetTodosUseCase(source: getTodosSource) 23 | let homeViewModel = HomeViewModel(getTodosUseCase: getTodosUseCase) 24 | return UIHostingController(rootView: HomeView(viewModel: homeViewModel)) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /MMA/Home/Data/Local/TodosDataSourceLocal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosDataSourceLocal.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TodosDataSourceLocal { 11 | func fetchTodos() async -> Result<[Todo], GetTodoError> 12 | } 13 | 14 | class TodosDataSourceLocalStub: TodosDataSourceLocal { 15 | let response: Result<[Todo], GetTodoError> 16 | 17 | init(response: Result<[Todo], GetTodoError>) { 18 | self.response = response 19 | } 20 | 21 | func fetchTodos() -> Result<[Todo], GetTodoError> { 22 | response 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MMA/Home/Data/Remote/TodosDataSourceRemote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosDataSourceRemote.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TodosDataSourceRemote { 11 | func fetchTodos() async -> Result<[Todo], GetTodoError> 12 | } 13 | 14 | class TodosDataSourceRemoteStub: TodosDataSourceRemote { 15 | let response: Result<[Todo], GetTodoError> 16 | 17 | init(response: Result<[Todo], GetTodoError>) { 18 | self.response = response 19 | } 20 | 21 | func fetchTodos() -> Result<[Todo], GetTodoError> { 22 | response 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MMA/Home/Data/Repository/GetTodosRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetTodosRepository.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class GetTodosRepository: GetTodosSource { 11 | let todosRemoteSource: TodosDataSourceRemote 12 | let todosLocalSource: TodosDataSourceLocal 13 | 14 | init(todosRemoteSource: TodosDataSourceRemote, todosLocalSource: TodosDataSourceLocal) { 15 | self.todosRemoteSource = todosRemoteSource 16 | self.todosLocalSource = todosLocalSource 17 | } 18 | 19 | func getTodos() async -> Result<[Todo], GetTodoError> { 20 | let todosLocalSourceResponse = await todosLocalSource.fetchTodos() 21 | switch todosLocalSourceResponse { 22 | case let .success(todos): 23 | if todos.isEmpty { 24 | return await todosRemoteSource.fetchTodos() 25 | } 26 | return todosLocalSourceResponse 27 | case .failure: 28 | return await todosRemoteSource.fetchTodos() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MMA/Home/Domain/Entities/Errors/GetTodoError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetTodoError.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum GetTodoError: Error, Hashable, Identifiable, Equatable, LocalizedError { 11 | var id: Self { self } 12 | 13 | case networkError(cause: String) 14 | case localStorageError(cause: String) 15 | 16 | var errorDescription: String? { 17 | switch self { 18 | case .networkError(let cause): 19 | return cause 20 | case .localStorageError(let cause): 21 | return cause 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MMA/Home/Domain/Entities/Errors/UpdateTodoError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateTodoError.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UpdateTodoError: Error, Equatable, LocalizedError { 11 | case localStorageError(cause: String) 12 | 13 | var errorDescription: String? { 14 | switch self { 15 | case .localStorageError(let cause): 16 | return cause 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MMA/Home/Domain/Entities/Todo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Todo.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/17/23. 6 | // 7 | 8 | struct Todo: Identifiable, Equatable { 9 | let userId: Int 10 | let id: Int 11 | let title: String 12 | let completed: Bool 13 | } 14 | 15 | extension Todo: Codable {} 16 | -------------------------------------------------------------------------------- /MMA/Home/Domain/Use Cases/Data Sources/GetTodosSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetTodosSource.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol GetTodosSource { 11 | func getTodos() async -> Result<[Todo], GetTodoError> 12 | } 13 | 14 | class GetTodosSourceStub: GetTodosSource { 15 | let response: Result<[Todo], GetTodoError> 16 | 17 | init(response: Result<[Todo], GetTodoError>) { 18 | self.response = response 19 | } 20 | 21 | func getTodos() -> Result<[Todo], GetTodoError> { 22 | response 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MMA/Home/Domain/Use Cases/GetTodosUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetTodosUseCase.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/17/23. 6 | // 7 | 8 | class GetTodosUseCase { 9 | let source: GetTodosSource 10 | 11 | init(source: GetTodosSource) { 12 | self.source = source 13 | } 14 | 15 | func getTodos() async -> Result<[Todo], GetTodoError> { 16 | await source.getTodos() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MMA/Home/Framework/Local/TodosDb.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosDb.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TodosDb { 11 | func getTodos() async -> Result<[Todo], GetTodoError> 12 | func updateTodos(with todos: [Todo]) async -> Result 13 | } 14 | 15 | class TodosDbStub: TodosDb { 16 | let getTodosResult: Result<[Todo], GetTodoError> 17 | let updateTodosResult: Result 18 | 19 | init( 20 | getTodosResult: Result<[Todo], GetTodoError> = .success([Todo(userId: 1, id: 1, title: "title", completed: false)]), 21 | updateTodosResult: Result = .success(()) 22 | ) { 23 | self.getTodosResult = getTodosResult 24 | self.updateTodosResult = updateTodosResult 25 | } 26 | 27 | func getTodos() async -> Result<[Todo], GetTodoError> { 28 | getTodosResult 29 | } 30 | 31 | func updateTodos(with todos: [Todo]) async -> Result { 32 | updateTodosResult 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MMA/Home/Framework/Local/TodosDbDataGateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosDbDataGateway.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | class TodosDbDataGateway: TodosDataSourceLocal { 9 | let db: TodosDb 10 | 11 | init(db: TodosDb) { 12 | self.db = db 13 | } 14 | 15 | func fetchTodos() async -> Result<[Todo], GetTodoError> { 16 | await db.getTodos() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MMA/Home/Framework/Local/TodosDbImp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosDbImp.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class TodosDbImp: TodosDb { 11 | var directoryURL: URL? 12 | 13 | init(directoryURL: URL? = nil) { 14 | self.directoryURL = directoryURL 15 | } 16 | 17 | func getTodos() async -> Result<[Todo], GetTodoError> { 18 | do { 19 | let todos = try await loadTodos() 20 | return .success(todos) 21 | } catch { 22 | return .failure(.localStorageError(cause: error.localizedDescription)) 23 | } 24 | } 25 | 26 | func updateTodos(with todos: [Todo]) async -> Result { 27 | do { 28 | try await save(todos: todos) 29 | return .success(()) 30 | } catch { 31 | return .failure(.localStorageError(cause: error.localizedDescription)) 32 | } 33 | } 34 | 35 | // MARK: - FileManager 36 | 37 | private func fileURL() throws -> URL { 38 | try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) 39 | .appendingPathComponent("todos.data") 40 | } 41 | 42 | private func loadTodos() async throws -> [Todo] { 43 | let task = Task<[Todo], Error> { 44 | let fileURL = try directoryURL ?? fileURL() 45 | guard let data = try? Data(contentsOf: fileURL) else { return [] } 46 | let todos = try JSONDecoder().decode([Todo].self, from: data) 47 | return todos 48 | } 49 | return try await task.value 50 | } 51 | 52 | private func save(todos: [Todo]) async throws { 53 | let task = Task { 54 | let data = try JSONEncoder().encode(todos) 55 | let outfile = try directoryURL ?? fileURL() 56 | try data.write(to: outfile) 57 | } 58 | _ = try await task.value 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /MMA/Home/Framework/Remote/TodosRemoteDataGateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosRemoteDataGateway.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | class TodosRemoteDataGateway: TodosDataSourceRemote { 9 | let service: TodosService 10 | let db: TodosDb 11 | 12 | init(service: TodosService, db: TodosDb) { 13 | self.service = service 14 | self.db = db 15 | } 16 | 17 | func fetchTodos() async -> Result<[Todo], GetTodoError> { 18 | let fetchTodosResponse = await service.fetchTodos() 19 | switch fetchTodosResponse { 20 | case let .success(todos): 21 | let _ = await db.updateTodos(with: todos) 22 | return .success(todos) 23 | case let .failure(error): 24 | return .failure(error) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MMA/Home/Framework/Remote/TodosService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosService.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | protocol TodosService { 9 | func fetchTodos() async -> Result<[Todo], GetTodoError> 10 | // More todos requests here... 11 | } 12 | 13 | class TodosServiceStub: TodosService { 14 | let fetchTodosResult: Result<[Todo], GetTodoError> 15 | 16 | init( 17 | fetchTodosResult: Result<[Todo], GetTodoError> = .success([Todo(userId: 1, id: 1, title: "title", completed: false)]) 18 | ) { 19 | self.fetchTodosResult = fetchTodosResult 20 | } 21 | 22 | func fetchTodos() async -> Result<[Todo], GetTodoError> { 23 | fetchTodosResult 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MMA/Home/Framework/Remote/TodosServiceImp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosServiceImp.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class TodosServiceImp: TodosService { 11 | private let urlSession: URLSession 12 | 13 | init(urlSession: URLSession = .shared) { 14 | self.urlSession = urlSession 15 | } 16 | 17 | func fetchTodos() async -> Result<[Todo], GetTodoError> { 18 | let urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos")!) 19 | do { 20 | let (data, urlResponse) = try await urlSession.data(for: urlRequest) 21 | guard let urlResponse = urlResponse as? HTTPURLResponse else { 22 | return .failure(.networkError(cause: "HTTPURLResponse cast error")) 23 | } 24 | guard urlResponse.statusCode == 200 else { 25 | return .failure(.networkError(cause: "HTTPURLResponse statusCode was not 200")) 26 | } 27 | let todos = try JSONDecoder().decode([Todo].self, from: data) 28 | return .success(todos) 29 | } catch { 30 | return .failure(.networkError(cause: error.localizedDescription)) 31 | } 32 | } 33 | 34 | // More todos requests here... 35 | } 36 | -------------------------------------------------------------------------------- /MMA/Home/Framework/Views/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeView: View { 11 | @ObservedObject var viewModel: HomeViewModel 12 | 13 | var body: some View { 14 | List { 15 | ForEach(viewModel.todos) { todo in 16 | HStack { 17 | Text(todo.title) 18 | Spacer() 19 | Image(systemName: todo.completed ? "checkmark.circle.fill" : "x.circle.fill") 20 | .foregroundColor(todo.completed ? .green : .pink) 21 | } 22 | .listRowBackground(Color.blue.opacity(0.5)) 23 | } 24 | } 25 | .sheet(item: $viewModel.alertError) { error in 26 | Text(error.localizedDescription) 27 | } 28 | .background(.cyan.gradient) 29 | .toolbarBackground(.cyan.gradient, for: .navigationBar) 30 | .scrollContentBackground(.hidden) 31 | .navigationTitle("Todos") 32 | .task { 33 | await viewModel.onAppearAction() 34 | } 35 | .refreshable { 36 | Task { 37 | await viewModel.refreshListAction() 38 | } 39 | } 40 | } 41 | } 42 | 43 | struct HomeView_Previews: PreviewProvider { 44 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: true) 45 | static let todo2 = Todo(userId: 2, id: 2, title: "title 2", completed: false) 46 | static let getTodosSource = GetTodosSourceStub(response: .success([todo, todo2])) 47 | static let getTodosUseCase = GetTodosUseCase(source: getTodosSource) 48 | static let homeViewModel = HomeViewModel(getTodosUseCase: getTodosUseCase) 49 | 50 | static var previews: some View { 51 | NavigationStack { 52 | HomeView(viewModel: homeViewModel) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MMA/Home/Presentation/HomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor 11 | class HomeViewModel: ObservableObject { 12 | @Published var todos = [Todo]() 13 | @Published var alertError: GetTodoError? 14 | 15 | let getTodosUseCase: GetTodosUseCase 16 | 17 | init(getTodosUseCase: GetTodosUseCase) { 18 | self.getTodosUseCase = getTodosUseCase 19 | } 20 | 21 | private func getTodos() async { 22 | let todosResult = await getTodosUseCase.getTodos() 23 | switch todosResult { 24 | case let .success(todos): 25 | self.todos = todos 26 | case let .failure(getTodoError): 27 | alertError = getTodoError 28 | } 29 | } 30 | 31 | func onAppearAction() async { 32 | await getTodos() 33 | } 34 | 35 | func refreshListAction() async { 36 | await getTodos() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MMA/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /MMA/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // MMA 4 | // 5 | // Created by Cesar Mejia Valero on 4/17/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | var appCoordinator: Coordinator! 14 | var appFactory: AppFactory! 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let scene = (scene as? UIWindowScene) else { return } 21 | let navigation = UINavigationController() 22 | appFactory = AppFactoryImp() 23 | window = UIWindow(windowScene: scene) 24 | appCoordinator = AppCoordinator(navigation: navigation, appFactory: appFactory, window: window) 25 | appCoordinator.start() 26 | } 27 | 28 | func sceneDidDisconnect(_ scene: UIScene) { 29 | // Called as the scene is being released by the system. 30 | // This occurs shortly after the scene enters the background, or when its session is discarded. 31 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 32 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 33 | } 34 | 35 | func sceneDidBecomeActive(_ scene: UIScene) { 36 | // Called when the scene has moved from an inactive state to an active state. 37 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 38 | } 39 | 40 | func sceneWillResignActive(_ scene: UIScene) { 41 | // Called when the scene will move from an active state to an inactive state. 42 | // This may occur due to temporary interruptions (ex. an incoming phone call). 43 | } 44 | 45 | func sceneWillEnterForeground(_ scene: UIScene) { 46 | // Called as the scene transitions from the background to the foreground. 47 | // Use this method to undo the changes made on entering the background. 48 | } 49 | 50 | func sceneDidEnterBackground(_ scene: UIScene) { 51 | // Called as the scene transitions from the foreground to the background. 52 | // Use this method to save data, release shared resources, and store enough scene-specific state information 53 | // to restore the scene back to its current state. 54 | } 55 | 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /MMATests/Data/GetTodosRepositoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetTodosRepositoryTests.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import XCTest 9 | @testable import MMA 10 | 11 | final class GetTodosRepositoryTests: XCTestCase { 12 | 13 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: true) 14 | static let todo2 = Todo(userId: 2, id: 2, title: "title", completed: true) 15 | 16 | func testGetTodosRepository_whenGettingTodosFromLocalSourceSuccessfuly_returnsTodos() async { 17 | let sut = makeSUT() 18 | let getTodoResult = await sut.getTodos() 19 | XCTAssertEqual(Result.success([Self.todo]), getTodoResult) 20 | } 21 | 22 | func testGetTodosRepository_whenGettingTodosFromLocalSourceIsEmpty_returnsRemoteTodos() async { 23 | let sut = makeSUT(todosLocalSource: TodosDataSourceLocalStub(response: .success([]))) 24 | let getTodoResult = await sut.getTodos() 25 | XCTAssertEqual(Result.success([Self.todo2]), getTodoResult) 26 | } 27 | 28 | func testGetTodosRepository_whenGettingTodosFromLocalSourceFails_returnsRemoteTodos() async { 29 | let sut = makeSUT(todosLocalSource: TodosDataSourceLocalStub(response: .failure(.localStorageError(cause: "localError")))) 30 | let getTodoResult = await sut.getTodos() 31 | XCTAssertEqual(Result.success([Self.todo2]), getTodoResult) 32 | } 33 | 34 | func testGetTodosRepository_whenGettingTodosFromBothSourcesFail_returnsNetworkError() async { 35 | let sut = makeSUT( 36 | todosRemoteSource: TodosDataSourceRemoteStub(response: .failure(.networkError(cause: "remoteError"))), 37 | todosLocalSource: TodosDataSourceLocalStub(response: .failure(.localStorageError(cause: "localError"))) 38 | ) 39 | let getTodoResult = await sut.getTodos() 40 | XCTAssertEqual(Result.failure(GetTodoError.networkError(cause: "remoteError")), getTodoResult) 41 | } 42 | 43 | // MARK: - Helpers 44 | 45 | private func makeSUT( 46 | todosRemoteSource: TodosDataSourceRemote = TodosDataSourceRemoteStub(response: .success([todo2])), 47 | todosLocalSource: TodosDataSourceLocal = TodosDataSourceLocalStub(response: .success([todo])), 48 | file: StaticString = #file, 49 | line: UInt = #line 50 | ) -> GetTodosRepository { 51 | let sut = GetTodosRepository( 52 | todosRemoteSource: todosRemoteSource, 53 | todosLocalSource: todosLocalSource) 54 | trackForMemoryLeaks(sut, file: file, line: line) 55 | return sut 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /MMATests/Domain/GetTodosUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetTodosUseCaseTests.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/17/23. 6 | // 7 | 8 | import XCTest 9 | @testable import MMA 10 | 11 | final class GetTodosUseCaseTests: XCTestCase { 12 | 13 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: true) 14 | 15 | func testGetTodosUseCase_whenCallingGetTodosIsSuccessful_getsSuccessfulResponse() async { 16 | let sut = makeSUT() 17 | let getTodoResult = await sut.getTodos() 18 | XCTAssertEqual(Result.success([Self.todo]), getTodoResult) 19 | } 20 | 21 | func testGetTodosUseCase_whenCallingGetTodosFails_getsErrorResponse() async { 22 | let getTodosSource = GetTodosSourceStub(response: .failure(.networkError(cause: "cause"))) 23 | let sut = makeSUT(getTodosSource: getTodosSource) 24 | let getTodoResult = await sut.getTodos() 25 | XCTAssertEqual(Result.failure(.networkError(cause: "cause")), getTodoResult) 26 | } 27 | 28 | // MARK: - Helpers 29 | 30 | private func makeSUT( 31 | getTodosSource: GetTodosSource = GetTodosSourceStub(response: .success([todo])), 32 | file: StaticString = #file, 33 | line: UInt = #line 34 | ) -> GetTodosUseCase { 35 | let sut = GetTodosUseCase( 36 | source: getTodosSource 37 | ) 38 | trackForMemoryLeaks(sut, file: file, line: line) 39 | return sut 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MMATests/Framework/Local/TodosDbDataGatewayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosDbDataGatewayTests.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/19/23. 6 | // 7 | 8 | import XCTest 9 | @testable import MMA 10 | 11 | final class TodosDbDataGatewayTests: XCTestCase { 12 | 13 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: false) 14 | 15 | func testTodosDbDataGateway_whenResultIsSuccessful_returnsTodos() async { 16 | let sut = makeSUT() 17 | let todosResult = await sut.fetchTodos() 18 | switch todosResult { 19 | case let .success(todos): 20 | XCTAssertEqual(todos, [Self.todo]) 21 | case .failure: 22 | XCTFail("Request should have succeded") 23 | } 24 | } 25 | 26 | func testTodosDbDataGateway_whenResultIsAFailure_returnsGetTodoError() async { 27 | let localStorageError = GetTodoError.localStorageError(cause: "local storage error") 28 | let todosDbStub = TodosDbStub(getTodosResult: .failure(localStorageError)) 29 | let sut = makeSUT(db: todosDbStub) 30 | let todosResult = await sut.fetchTodos() 31 | switch todosResult { 32 | case .success: 33 | XCTFail("Request should have failed") 34 | case let .failure(error): 35 | XCTAssertEqual(error, localStorageError) 36 | } 37 | } 38 | 39 | // MARK: - Helpers 40 | 41 | private func makeSUT( 42 | db: TodosDb = TodosDbStub(), 43 | file: StaticString = #file, 44 | line: UInt = #line 45 | ) -> TodosDbDataGateway { 46 | let sut = TodosDbDataGateway(db: db) 47 | trackForMemoryLeaks(sut, file: file, line: line) 48 | return sut 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MMATests/Framework/Local/TodosDbImpTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosDbImpTests.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import XCTest 9 | @testable import MMA 10 | 11 | final class TodosDbImpTests: XCTestCase { 12 | 13 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: true) 14 | static let todo2 = Todo(userId: 2, id: 2, title: "title", completed: true) 15 | 16 | static let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 17 | 18 | override func tearDownWithError() throws { 19 | super.tearDown() 20 | try FileManager.default.removeItem(at: Self.temporaryDirectoryURL) 21 | } 22 | 23 | func testTodosDbImp_whenRequestingGetTodosInitially_getsAnEmptyTodoList() async { 24 | let sut = makeSUT() 25 | let todosResult = await sut.getTodos() 26 | 27 | switch todosResult { 28 | case let .success(todos): 29 | XCTAssertEqual(todos, []) 30 | default: 31 | XCTFail("Request should have succeded") 32 | } 33 | } 34 | 35 | func testTodosDbImp_whenUpdatingTodos_todosAreSavedInADirectory() async { 36 | let todoList = [Self.todo, Self.todo2] 37 | let sut = makeSUT() 38 | _ = await sut.updateTodos(with: todoList) 39 | let todosResult = await sut.getTodos() 40 | 41 | switch todosResult { 42 | case let .success(todos): 43 | XCTAssertEqual(todos, todoList) 44 | default: 45 | XCTFail("Request should have succeded") 46 | } 47 | } 48 | 49 | // MARK: - Helpers 50 | 51 | private func makeSUT( 52 | file: StaticString = #file, 53 | line: UInt = #line 54 | ) -> TodosDbImp { 55 | let sut = TodosDbImp(directoryURL: Self.temporaryDirectoryURL) 56 | trackForMemoryLeaks(sut, file: file, line: line) 57 | return sut 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MMATests/Framework/Remote/TodosRemoteDataGatewayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosRemoteDataGatewayTests.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/19/23. 6 | // 7 | 8 | import XCTest 9 | @testable import MMA 10 | 11 | final class TodosRemoteDataGatewayTests: XCTestCase { 12 | 13 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: false) 14 | 15 | func testTodosRemoteDataGateway_whenResultIsSuccessful_returnsTodos() async { 16 | let sut = makeSUT() 17 | let todosResult = await sut.fetchTodos() 18 | switch todosResult { 19 | case let .success(todos): 20 | XCTAssertEqual(todos, [Self.todo]) 21 | case .failure: 22 | XCTFail("Request should have succeded") 23 | } 24 | } 25 | 26 | func testTodosRemoteDataGateway_whenResultIsAFailure_returnsGetTodoError() async { 27 | let remoteStorageError = GetTodoError.networkError(cause: "network error") 28 | let todosServiceStub = TodosServiceStub(fetchTodosResult: .failure(remoteStorageError)) 29 | let sut = makeSUT(service: todosServiceStub) 30 | let todosResult = await sut.fetchTodos() 31 | switch todosResult { 32 | case .success: 33 | XCTFail("Request should have failed") 34 | case let .failure(error): 35 | XCTAssertEqual(error, remoteStorageError) 36 | } 37 | } 38 | 39 | func testTodosRemoteDataGateway_whenResultIsSuccessful_updatesDbTodos() async { 40 | let todosDbSpy = TodosDbSpy() 41 | let sut = makeSUT(db: todosDbSpy) 42 | let todosResult = await sut.fetchTodos() 43 | XCTAssertTrue(todosDbSpy.didUpdateTodos) 44 | } 45 | 46 | // MARK: - Helpers 47 | 48 | private func makeSUT( 49 | service: TodosService = TodosServiceStub(), 50 | db: TodosDb = TodosDbStub(), 51 | file: StaticString = #file, 52 | line: UInt = #line 53 | ) -> TodosRemoteDataGateway { 54 | let sut = TodosRemoteDataGateway(service: service, db: db) 55 | trackForMemoryLeaks(sut, file: file, line: line) 56 | return sut 57 | } 58 | 59 | class TodosDbSpy: TodosDbStub { 60 | var didUpdateTodos = false 61 | 62 | override func updateTodos(with todos: [Todo]) async -> Result { 63 | didUpdateTodos = true 64 | return updateTodosResult 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /MMATests/Framework/Remote/TodosServiceImpTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodosServiceImpTests.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import XCTest 9 | @testable import MMA 10 | 11 | final class TodosServiceImpTests: XCTestCase { 12 | 13 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: true) 14 | static let todo2 = Todo(userId: 2, id: 2, title: "title", completed: true) 15 | 16 | override func tearDown() { 17 | super.tearDown() 18 | 19 | URLProtocolStub.removeStub() 20 | } 21 | 22 | func testTodosServiceImp_whenFetchingTodosRequestSucceeds_returnsTodos() async { 23 | let url = URL(string: "https://jsonplaceholder.typicode.com")! 24 | 25 | let todoList = [Self.todo, Self.todo2] 26 | let encodedTodoList = try? JSONEncoder().encode(todoList.self) 27 | let urlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! 28 | URLProtocolStub.stub(data: encodedTodoList, response: urlResponse, error: nil) 29 | 30 | let sut = makeSUT() 31 | let todosResult = await sut.fetchTodos() 32 | switch todosResult { 33 | case let .success(todos): 34 | XCTAssertEqual(todos, todoList) 35 | default: 36 | XCTFail("Request should have succeded") 37 | } 38 | } 39 | 40 | // MARK: - Helpers 41 | 42 | private func makeSUT( 43 | file: StaticString = #file, 44 | line: UInt = #line 45 | ) -> TodosServiceImp { 46 | let configuration = URLSessionConfiguration.ephemeral 47 | configuration.protocolClasses = [URLProtocolStub.self] 48 | let urlSession = URLSession(configuration: configuration) 49 | 50 | let sut = TodosServiceImp(urlSession: urlSession) 51 | trackForMemoryLeaks(sut, file: file, line: line) 52 | return sut 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MMATests/Helpers/URLProtocolStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLProtocolStub.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class URLProtocolStub: URLProtocol { 11 | private struct Stub { 12 | let data: Data? 13 | let response: URLResponse? 14 | let error: Error? 15 | let requestObserver: ((URLRequest) -> Void)? 16 | } 17 | 18 | private static var _stub: Stub? 19 | private static var stub: Stub? { 20 | get { return queue.sync { _stub } } 21 | set { queue.sync { _stub = newValue } } 22 | } 23 | 24 | private static let queue = DispatchQueue(label: "URLProtocolStub.queue") 25 | 26 | static func stub(data: Data?, response: URLResponse?, error: Error?) { 27 | stub = Stub(data: data, response: response, error: error, requestObserver: nil) 28 | } 29 | 30 | static func observeRequests(observer: @escaping (URLRequest) -> Void) { 31 | stub = Stub(data: nil, response: nil, error: nil, requestObserver: observer) 32 | } 33 | 34 | static func removeStub() { 35 | stub = nil 36 | } 37 | 38 | override class func canInit(with request: URLRequest) -> Bool { 39 | return true 40 | } 41 | 42 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 43 | return request 44 | } 45 | 46 | override func startLoading() { 47 | guard let stub = URLProtocolStub.stub else { return } 48 | 49 | if let data = stub.data { 50 | client?.urlProtocol(self, didLoad: data) 51 | } 52 | 53 | if let response = stub.response { 54 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 55 | } 56 | 57 | if let error = stub.error { 58 | client?.urlProtocol(self, didFailWithError: error) 59 | } else { 60 | client?.urlProtocolDidFinishLoading(self) 61 | } 62 | 63 | stub.requestObserver?(request) 64 | } 65 | 66 | override func stopLoading() {} 67 | } 68 | import Foundation 69 | -------------------------------------------------------------------------------- /MMATests/Helpers/XCTestCase+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestCase+Extensions.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import XCTest 9 | 10 | extension XCTestCase { 11 | func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #file, line: UInt = #line) { 12 | addTeardownBlock { [weak instance] in 13 | XCTAssertNil(instance, "Instance should have been deallocated, Potential memory leak", file: file, line: line) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MMATests/Presentation/HomeViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModelTests.swift 3 | // MMATests 4 | // 5 | // Created by Cesar Mejia Valero on 4/18/23. 6 | // 7 | 8 | import XCTest 9 | @testable import MMA 10 | 11 | final class HomeViewModelTests: XCTestCase { 12 | 13 | static let todo = Todo(userId: 1, id: 1, title: "title", completed: true) 14 | static let todo2 = Todo(userId: 2, id: 2, title: "title", completed: true) 15 | static let todosDataSourceRemoteStub = TodosDataSourceRemoteStub(response: .success([todo])) 16 | static let todosDataSourceLocalStub = TodosDataSourceLocalStub(response: .success([todo2])) 17 | static let getTodosSource = buildGetTodosRepository(todosRemoteSource: todosDataSourceRemoteStub, todosLocalSource: todosDataSourceLocalStub) 18 | static let getTodosUseCase = GetTodosUseCase(source: getTodosSource) 19 | 20 | @MainActor 21 | func testHomeViewModel_whenOnAppear_todosArePopulated() async { 22 | let sut = makeSUT() 23 | await sut.onAppearAction() 24 | XCTAssertFalse(sut.todos.isEmpty) 25 | } 26 | 27 | @MainActor 28 | func testHomeViewModel_whenOnAppear_todosArePopulatedWithLocalTodos() async { 29 | let sut = makeSUT() 30 | await sut.onAppearAction() 31 | XCTAssertEqual(sut.todos, [Self.todo2]) 32 | } 33 | 34 | @MainActor 35 | func testHomeViewModel_whenOnAppearGetTodosRemoteFails_errorAlertCauseIsSet() async { 36 | let remoteErrorCause = "Remote Fetch failed" 37 | let localErrorCause = "Local Storage failed" 38 | let getTodoErrorNetworkError = GetTodoError.networkError(cause: remoteErrorCause) 39 | let todosDataSourceRemoteStubWithError = TodosDataSourceRemoteStub(response: .failure(getTodoErrorNetworkError)) 40 | let todosDataSourceLocalStubWithError = TodosDataSourceLocalStub(response: .failure(.localStorageError(cause: localErrorCause))) 41 | let getTodosSource = Self.buildGetTodosRepository(todosRemoteSource: todosDataSourceRemoteStubWithError, todosLocalSource: todosDataSourceLocalStubWithError) 42 | let getTodosUseCase = GetTodosUseCase(source: getTodosSource) 43 | let sut = makeSUT(getTodosUseCase: getTodosUseCase) 44 | await sut.onAppearAction() 45 | XCTAssertEqual(sut.alertError, getTodoErrorNetworkError) 46 | } 47 | 48 | // MARK: - Helpers 49 | 50 | @MainActor 51 | private func makeSUT( 52 | getTodosUseCase: GetTodosUseCase = getTodosUseCase, 53 | file: StaticString = #file, 54 | line: UInt = #line 55 | ) -> HomeViewModel { 56 | let sut = HomeViewModel(getTodosUseCase: getTodosUseCase) 57 | trackForMemoryLeaks(sut, file: file, line: line) 58 | return sut 59 | } 60 | 61 | private static func buildGetTodosRepository( 62 | todosRemoteSource: TodosDataSourceRemote = todosDataSourceRemoteStub, 63 | todosLocalSource: TodosDataSourceLocal = todosDataSourceLocalStub 64 | ) -> GetTodosRepository { 65 | return GetTodosRepository( 66 | todosRemoteSource: todosRemoteSource, 67 | todosLocalSource: todosLocalSource) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI-Clean-Architecture-example-with-unit-tests 2 | 3 | This is a small project that gets a TODO list from the network, stores it with Filemanager and shows it in a SwiftUI list. 4 | 5 | A second version of this repo is here: https://github.com/cesmejia/SwiftUI-Reactive-Clean-Architecture-MVVM 6 | It adds reactivity and a more complete coordinator pattern with tabs 7 | 8 | ## Overview 9 | 10 | The project was developed with the following concepts in mind: 11 | 12 | - ``No external libraries`` 13 | - ``SOLID principles`` 14 | - ``Clean Architecture`` 15 | - ``MVVM Architecture`` 16 | - ``Use of Composition root`` 17 | - ``Coordinator Pattern: Uses UIKit UINavigationController + UIHostingController for navigation`` 18 | - ``Factory Pattern`` 19 | - ``Repository Pattern`` 20 | - ``Use Cases`` 21 | - ``Async Await + Result APIs`` 22 | - ``Dependency Injection`` 23 | - ``Unit tests: Although TDD was not used, tests were created after each instance creation`` 24 | - ``Test doubles: Use of Stubs, Spys and Mocks`` 25 | - ``Test for memory leaks`` 26 | - ``Folder separation: Domain, Data, Presentation and Framework`` 27 | 28 | ### Dependency Diagram: 29 | 30 | ![Clean MVVM SwiftUI](https://user-images.githubusercontent.com/24886388/233222696-eddef548-90d9-4930-b7bb-83eec2c9fdb4.jpg) 31 | 32 | ### Disclaimer: 33 | 34 | This is a very basic project to serve as guide for a tested Clean Architecture approach with SwiftUI. 35 | 36 | - Feedback is welcomed. 37 | - I might add some more use cases and features in the near future. 38 | - TODO entity was used throughout the app for simplification sake. True modularity would be achieved by mapping it between layers. 39 | 40 | ### Useful resources that made this possible: 41 | 42 | - Essential developer course: [Essential Developer](https://www.essentialdeveloper.com) 43 | - Hacking with swift: [HWS](https://www.hackingwithswift.com) 44 | - Clean Mobile Architecture Book by Petros Efthymiou: [Clean Mobile Architecture](https://www.petrosefthymiou.com/product-page/clean-mobile-architecture) 45 | - Dependency Injection Principles, Practices, and Patterns by Mark Seemann and Steven van Deursen [Dependency Injection](https://www.goodreads.com/en/book/show/44416307-dependency-injection-principles-practices-and-patterns) 46 | - Clean Architecture Book by Robert C. Martin (Uncle Bob) [Clean Architecture](https://www.goodreads.com/book/show/18043011-clean-architecture?ref=nav_sb_ss_1_11) 47 | --------------------------------------------------------------------------------