├── ExampleMVVM.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── macbookairm1.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── macbookairm1.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── ExampleMVVM ├── App │ └── ExampleMVVMApp.swift ├── Core │ └── Extensions │ │ └── Extension+Date.swift ├── Data │ ├── Local │ │ └── LocalWeatherDataSource.swift │ ├── Remote │ │ └── RemoteWeatherDataSource.swift │ └── Repositories │ │ └── WeatherRepositoryImpl.swift ├── Domain │ ├── Entities │ │ ├── City.swift │ │ ├── Temperature.swift │ │ └── Weather.swift │ ├── Interfaces │ │ ├── WeatherDataSource.swift │ │ └── WeatherRepository.swift │ └── UseCases │ │ └── FetchWeatherUseCase.swift ├── Infrastructure │ ├── Interfaces │ │ ├── APIClientProtocol.swift │ │ ├── ConfigurationProtocol.swift │ │ └── JSONLoaderProtocol.swift │ ├── Network │ │ ├── APIClient.swift │ │ ├── Configuration │ │ │ └── ConfigurationManager.swift │ │ ├── HTTPMethod.swift │ │ └── WeatherAPIConfiguration.swift │ └── Utilities │ │ └── JSONLoader.swift ├── Presentation │ └── Weather │ │ ├── View │ │ ├── WeatherRowView.swift │ │ └── WeatherView.swift │ │ └── ViewModel │ │ └── WeatherViewModel.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Resources │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ └── weather_data.json ├── ExampleMVVMTests ├── Core │ └── ConfigurationManagerTests.swift ├── Data │ └── WeatherRepositoryTests.swift ├── Domain │ └── FetchWeatherUseCaseTests.swift ├── ExampleMVVMTests.swift ├── Infrastructure │ └── APIClientTests.swift └── Presentation │ └── WeatherViewModelTests.swift ├── ExampleMVVMUITests ├── ExampleMVVMUITests.swift └── ExampleMVVMUITestsLaunchTests.swift └── README.md /ExampleMVVM.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6D139EB12C23342A004DF286 /* ExampleMVVMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EB02C23342A004DF286 /* ExampleMVVMApp.swift */; }; 11 | 6D139EB52C23342C004DF286 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D139EB42C23342C004DF286 /* Assets.xcassets */; }; 12 | 6D139EB82C23342C004DF286 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D139EB72C23342C004DF286 /* Preview Assets.xcassets */; }; 13 | 6D139EC22C23342C004DF286 /* ExampleMVVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EC12C23342C004DF286 /* ExampleMVVMTests.swift */; }; 14 | 6D139ECC2C23342D004DF286 /* ExampleMVVMUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139ECB2C23342D004DF286 /* ExampleMVVMUITests.swift */; }; 15 | 6D139ECE2C23342D004DF286 /* ExampleMVVMUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139ECD2C23342D004DF286 /* ExampleMVVMUITestsLaunchTests.swift */; }; 16 | 6D139EDF2C2334E7004DF286 /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EDE2C2334E7004DF286 /* Weather.swift */; }; 17 | 6D139EE12C233509004DF286 /* FetchWeatherUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EE02C233509004DF286 /* FetchWeatherUseCase.swift */; }; 18 | 6D139EE32C233536004DF286 /* WeatherRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EE22C233536004DF286 /* WeatherRepository.swift */; }; 19 | 6D139EE52C2335C0004DF286 /* City.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EE42C2335C0004DF286 /* City.swift */; }; 20 | 6D139EE72C2335E4004DF286 /* Temperature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EE62C2335E4004DF286 /* Temperature.swift */; }; 21 | 6D139EEB2C23368B004DF286 /* WeatherRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EEA2C23368B004DF286 /* WeatherRepositoryImpl.swift */; }; 22 | 6D139EF42C2337EC004DF286 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EF32C2337EC004DF286 /* WeatherViewModel.swift */; }; 23 | 6D139EF62C23380E004DF286 /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EF52C23380E004DF286 /* WeatherView.swift */; }; 24 | 6D139EF82C23381D004DF286 /* WeatherRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EF72C23381D004DF286 /* WeatherRowView.swift */; }; 25 | 6D139EFF2C233952004DF286 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139EFE2C233952004DF286 /* APIClient.swift */; }; 26 | 6D139F022C233D4E004DF286 /* WeatherAPIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F012C233D4E004DF286 /* WeatherAPIConfiguration.swift */; }; 27 | 6D139F042C23B8A6004DF286 /* Extension+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F032C23B8A6004DF286 /* Extension+Date.swift */; }; 28 | 6D139F072C23BB0F004DF286 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F062C23BB0F004DF286 /* ConfigurationManager.swift */; }; 29 | 6D139F092C23BB8D004DF286 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F082C23BB8D004DF286 /* HTTPMethod.swift */; }; 30 | 6D139F142C23C3AE004DF286 /* ConfigurationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F132C23C3AE004DF286 /* ConfigurationManagerTests.swift */; }; 31 | 6D139F162C23C3E5004DF286 /* APIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F152C23C3E5004DF286 /* APIClientTests.swift */; }; 32 | 6D139F182C23C42E004DF286 /* WeatherRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F172C23C42E004DF286 /* WeatherRepositoryTests.swift */; }; 33 | 6D139F1A2C23C450004DF286 /* FetchWeatherUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F192C23C450004DF286 /* FetchWeatherUseCaseTests.swift */; }; 34 | 6D139F1C2C23C47A004DF286 /* WeatherViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F1B2C23C47A004DF286 /* WeatherViewModelTests.swift */; }; 35 | 6D139F212C23C9C2004DF286 /* JSONLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F202C23C9C2004DF286 /* JSONLoader.swift */; }; 36 | 6D139F242C23CA2E004DF286 /* LocalWeatherDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F232C23CA2E004DF286 /* LocalWeatherDataSource.swift */; }; 37 | 6D139F292C23CE4E004DF286 /* RemoteWeatherDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F282C23CE4E004DF286 /* RemoteWeatherDataSource.swift */; }; 38 | 6D139F2B2C23CECB004DF286 /* WeatherDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F2A2C23CECB004DF286 /* WeatherDataSource.swift */; }; 39 | 6D139F2E2C23D06D004DF286 /* JSONLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F2D2C23D06D004DF286 /* JSONLoaderProtocol.swift */; }; 40 | 6D139F302C23D226004DF286 /* APIClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F2F2C23D226004DF286 /* APIClientProtocol.swift */; }; 41 | 6D139F322C23D505004DF286 /* ConfigurationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D139F312C23D505004DF286 /* ConfigurationProtocol.swift */; }; 42 | 6D139F342C23DA35004DF286 /* weather_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 6D139F332C23DA35004DF286 /* weather_data.json */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXContainerItemProxy section */ 46 | 6D139EBE2C23342C004DF286 /* PBXContainerItemProxy */ = { 47 | isa = PBXContainerItemProxy; 48 | containerPortal = 6D139EA52C23342A004DF286 /* Project object */; 49 | proxyType = 1; 50 | remoteGlobalIDString = 6D139EAC2C23342A004DF286; 51 | remoteInfo = ExampleMVVM; 52 | }; 53 | 6D139EC82C23342D004DF286 /* PBXContainerItemProxy */ = { 54 | isa = PBXContainerItemProxy; 55 | containerPortal = 6D139EA52C23342A004DF286 /* Project object */; 56 | proxyType = 1; 57 | remoteGlobalIDString = 6D139EAC2C23342A004DF286; 58 | remoteInfo = ExampleMVVM; 59 | }; 60 | /* End PBXContainerItemProxy section */ 61 | 62 | /* Begin PBXFileReference section */ 63 | 6D139EAD2C23342A004DF286 /* ExampleMVVM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleMVVM.app; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 6D139EB02C23342A004DF286 /* ExampleMVVMApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleMVVMApp.swift; sourceTree = ""; }; 65 | 6D139EB42C23342C004DF286 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 66 | 6D139EB72C23342C004DF286 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 67 | 6D139EBD2C23342C004DF286 /* ExampleMVVMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleMVVMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 6D139EC12C23342C004DF286 /* ExampleMVVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleMVVMTests.swift; sourceTree = ""; }; 69 | 6D139EC72C23342D004DF286 /* ExampleMVVMUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleMVVMUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 6D139ECB2C23342D004DF286 /* ExampleMVVMUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleMVVMUITests.swift; sourceTree = ""; }; 71 | 6D139ECD2C23342D004DF286 /* ExampleMVVMUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleMVVMUITestsLaunchTests.swift; sourceTree = ""; }; 72 | 6D139EDE2C2334E7004DF286 /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = ""; }; 73 | 6D139EE02C233509004DF286 /* FetchWeatherUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchWeatherUseCase.swift; sourceTree = ""; }; 74 | 6D139EE22C233536004DF286 /* WeatherRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherRepository.swift; sourceTree = ""; }; 75 | 6D139EE42C2335C0004DF286 /* City.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = City.swift; sourceTree = ""; }; 76 | 6D139EE62C2335E4004DF286 /* Temperature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Temperature.swift; sourceTree = ""; }; 77 | 6D139EEA2C23368B004DF286 /* WeatherRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherRepositoryImpl.swift; sourceTree = ""; }; 78 | 6D139EF32C2337EC004DF286 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; 79 | 6D139EF52C23380E004DF286 /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = ""; }; 80 | 6D139EF72C23381D004DF286 /* WeatherRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherRowView.swift; sourceTree = ""; }; 81 | 6D139EFE2C233952004DF286 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; 82 | 6D139F012C233D4E004DF286 /* WeatherAPIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPIConfiguration.swift; sourceTree = ""; }; 83 | 6D139F032C23B8A6004DF286 /* Extension+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+Date.swift"; sourceTree = ""; }; 84 | 6D139F062C23BB0F004DF286 /* ConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; }; 85 | 6D139F082C23BB8D004DF286 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 86 | 6D139F132C23C3AE004DF286 /* ConfigurationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManagerTests.swift; sourceTree = ""; }; 87 | 6D139F152C23C3E5004DF286 /* APIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientTests.swift; sourceTree = ""; }; 88 | 6D139F172C23C42E004DF286 /* WeatherRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherRepositoryTests.swift; sourceTree = ""; }; 89 | 6D139F192C23C450004DF286 /* FetchWeatherUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchWeatherUseCaseTests.swift; sourceTree = ""; }; 90 | 6D139F1B2C23C47A004DF286 /* WeatherViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModelTests.swift; sourceTree = ""; }; 91 | 6D139F202C23C9C2004DF286 /* JSONLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONLoader.swift; sourceTree = ""; }; 92 | 6D139F232C23CA2E004DF286 /* LocalWeatherDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalWeatherDataSource.swift; sourceTree = ""; }; 93 | 6D139F282C23CE4E004DF286 /* RemoteWeatherDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteWeatherDataSource.swift; sourceTree = ""; }; 94 | 6D139F2A2C23CECB004DF286 /* WeatherDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDataSource.swift; sourceTree = ""; }; 95 | 6D139F2D2C23D06D004DF286 /* JSONLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONLoaderProtocol.swift; sourceTree = ""; }; 96 | 6D139F2F2C23D226004DF286 /* APIClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientProtocol.swift; sourceTree = ""; }; 97 | 6D139F312C23D505004DF286 /* ConfigurationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationProtocol.swift; sourceTree = ""; }; 98 | 6D139F332C23DA35004DF286 /* weather_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weather_data.json; sourceTree = ""; }; 99 | /* End PBXFileReference section */ 100 | 101 | /* Begin PBXFrameworksBuildPhase section */ 102 | 6D139EAA2C23342A004DF286 /* Frameworks */ = { 103 | isa = PBXFrameworksBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | 6D139EBA2C23342C004DF286 /* Frameworks */ = { 110 | isa = PBXFrameworksBuildPhase; 111 | buildActionMask = 2147483647; 112 | files = ( 113 | ); 114 | runOnlyForDeploymentPostprocessing = 0; 115 | }; 116 | 6D139EC42C23342D004DF286 /* Frameworks */ = { 117 | isa = PBXFrameworksBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | ); 121 | runOnlyForDeploymentPostprocessing = 0; 122 | }; 123 | /* End PBXFrameworksBuildPhase section */ 124 | 125 | /* Begin PBXGroup section */ 126 | 6D139EA42C23342A004DF286 = { 127 | isa = PBXGroup; 128 | children = ( 129 | 6D139EAF2C23342A004DF286 /* ExampleMVVM */, 130 | 6D139EC02C23342C004DF286 /* ExampleMVVMTests */, 131 | 6D139ECA2C23342D004DF286 /* ExampleMVVMUITests */, 132 | 6D139EAE2C23342A004DF286 /* Products */, 133 | ); 134 | sourceTree = ""; 135 | }; 136 | 6D139EAE2C23342A004DF286 /* Products */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 6D139EAD2C23342A004DF286 /* ExampleMVVM.app */, 140 | 6D139EBD2C23342C004DF286 /* ExampleMVVMTests.xctest */, 141 | 6D139EC72C23342D004DF286 /* ExampleMVVMUITests.xctest */, 142 | ); 143 | name = Products; 144 | sourceTree = ""; 145 | }; 146 | 6D139EAF2C23342A004DF286 /* ExampleMVVM */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 6D139EF92C2338E8004DF286 /* Core */, 150 | 6D139F0A2C23BD83004DF286 /* Infrastructure */, 151 | 6D139EE82C233652004DF286 /* Data */, 152 | 6D139EDA2C23344D004DF286 /* Domain */, 153 | 6D139EEF2C233798004DF286 /* Presentation */, 154 | 6D139F002C233AB6004DF286 /* App */, 155 | 6D139F252C23CB04004DF286 /* Resources */, 156 | 6D139EB62C23342C004DF286 /* Preview Content */, 157 | ); 158 | path = ExampleMVVM; 159 | sourceTree = ""; 160 | }; 161 | 6D139EB62C23342C004DF286 /* Preview Content */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 6D139EB72C23342C004DF286 /* Preview Assets.xcassets */, 165 | ); 166 | path = "Preview Content"; 167 | sourceTree = ""; 168 | }; 169 | 6D139EC02C23342C004DF286 /* ExampleMVVMTests */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 6D139F122C23C36F004DF286 /* Core */, 173 | 6D139F112C23C363004DF286 /* Infrastructure */, 174 | 6D139F102C23C358004DF286 /* Data */, 175 | 6D139F0F2C23C349004DF286 /* Domain */, 176 | 6D139F0E2C23C33B004DF286 /* Presentation */, 177 | 6D139EC12C23342C004DF286 /* ExampleMVVMTests.swift */, 178 | ); 179 | path = ExampleMVVMTests; 180 | sourceTree = ""; 181 | }; 182 | 6D139ECA2C23342D004DF286 /* ExampleMVVMUITests */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 6D139ECB2C23342D004DF286 /* ExampleMVVMUITests.swift */, 186 | 6D139ECD2C23342D004DF286 /* ExampleMVVMUITestsLaunchTests.swift */, 187 | ); 188 | path = ExampleMVVMUITests; 189 | sourceTree = ""; 190 | }; 191 | 6D139EDA2C23344D004DF286 /* Domain */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | 6D139EDD2C2334AD004DF286 /* Interfaces */, 195 | 6D139EDC2C23349A004DF286 /* UseCases */, 196 | 6D139EDB2C233488004DF286 /* Entities */, 197 | ); 198 | path = Domain; 199 | sourceTree = ""; 200 | }; 201 | 6D139EDB2C233488004DF286 /* Entities */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 6D139EDE2C2334E7004DF286 /* Weather.swift */, 205 | 6D139EE42C2335C0004DF286 /* City.swift */, 206 | 6D139EE62C2335E4004DF286 /* Temperature.swift */, 207 | ); 208 | path = Entities; 209 | sourceTree = ""; 210 | }; 211 | 6D139EDC2C23349A004DF286 /* UseCases */ = { 212 | isa = PBXGroup; 213 | children = ( 214 | 6D139EE02C233509004DF286 /* FetchWeatherUseCase.swift */, 215 | ); 216 | path = UseCases; 217 | sourceTree = ""; 218 | }; 219 | 6D139EDD2C2334AD004DF286 /* Interfaces */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 6D139EE22C233536004DF286 /* WeatherRepository.swift */, 223 | 6D139F2A2C23CECB004DF286 /* WeatherDataSource.swift */, 224 | ); 225 | path = Interfaces; 226 | sourceTree = ""; 227 | }; 228 | 6D139EE82C233652004DF286 /* Data */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | 6D139F272C23CE3D004DF286 /* Remote */, 232 | 6D139F222C23CA17004DF286 /* Local */, 233 | 6D139EE92C23366F004DF286 /* Repositories */, 234 | ); 235 | path = Data; 236 | sourceTree = ""; 237 | }; 238 | 6D139EE92C23366F004DF286 /* Repositories */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 6D139EEA2C23368B004DF286 /* WeatherRepositoryImpl.swift */, 242 | ); 243 | path = Repositories; 244 | sourceTree = ""; 245 | }; 246 | 6D139EEF2C233798004DF286 /* Presentation */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | 6D139EF02C2337B0004DF286 /* Weather */, 250 | ); 251 | path = Presentation; 252 | sourceTree = ""; 253 | }; 254 | 6D139EF02C2337B0004DF286 /* Weather */ = { 255 | isa = PBXGroup; 256 | children = ( 257 | 6D139EF22C2337CF004DF286 /* ViewModel */, 258 | 6D139EF12C2337BA004DF286 /* View */, 259 | ); 260 | path = Weather; 261 | sourceTree = ""; 262 | }; 263 | 6D139EF12C2337BA004DF286 /* View */ = { 264 | isa = PBXGroup; 265 | children = ( 266 | 6D139EF52C23380E004DF286 /* WeatherView.swift */, 267 | 6D139EF72C23381D004DF286 /* WeatherRowView.swift */, 268 | ); 269 | path = View; 270 | sourceTree = ""; 271 | }; 272 | 6D139EF22C2337CF004DF286 /* ViewModel */ = { 273 | isa = PBXGroup; 274 | children = ( 275 | 6D139EF32C2337EC004DF286 /* WeatherViewModel.swift */, 276 | ); 277 | path = ViewModel; 278 | sourceTree = ""; 279 | }; 280 | 6D139EF92C2338E8004DF286 /* Core */ = { 281 | isa = PBXGroup; 282 | children = ( 283 | 6D139EFC2C2338FE004DF286 /* Utilities */, 284 | 6D139EFA2C2338F0004DF286 /* Extensions */, 285 | 6D139EFB2C2338F7004DF286 /* Common */, 286 | ); 287 | path = Core; 288 | sourceTree = ""; 289 | }; 290 | 6D139EFA2C2338F0004DF286 /* Extensions */ = { 291 | isa = PBXGroup; 292 | children = ( 293 | 6D139F032C23B8A6004DF286 /* Extension+Date.swift */, 294 | ); 295 | path = Extensions; 296 | sourceTree = ""; 297 | }; 298 | 6D139EFB2C2338F7004DF286 /* Common */ = { 299 | isa = PBXGroup; 300 | children = ( 301 | ); 302 | path = Common; 303 | sourceTree = ""; 304 | }; 305 | 6D139EFC2C2338FE004DF286 /* Utilities */ = { 306 | isa = PBXGroup; 307 | children = ( 308 | ); 309 | path = Utilities; 310 | sourceTree = ""; 311 | }; 312 | 6D139EFD2C233942004DF286 /* Network */ = { 313 | isa = PBXGroup; 314 | children = ( 315 | 6D139F0B2C23C111004DF286 /* Configuration */, 316 | 6D139F012C233D4E004DF286 /* WeatherAPIConfiguration.swift */, 317 | 6D139EFE2C233952004DF286 /* APIClient.swift */, 318 | 6D139F082C23BB8D004DF286 /* HTTPMethod.swift */, 319 | ); 320 | path = Network; 321 | sourceTree = ""; 322 | }; 323 | 6D139F002C233AB6004DF286 /* App */ = { 324 | isa = PBXGroup; 325 | children = ( 326 | 6D139EB02C23342A004DF286 /* ExampleMVVMApp.swift */, 327 | ); 328 | path = App; 329 | sourceTree = ""; 330 | }; 331 | 6D139F0A2C23BD83004DF286 /* Infrastructure */ = { 332 | isa = PBXGroup; 333 | children = ( 334 | 6D139F2C2C23D05A004DF286 /* Interfaces */, 335 | 6D139F1F2C23C9B0004DF286 /* Utilities */, 336 | 6D139EFD2C233942004DF286 /* Network */, 337 | ); 338 | path = Infrastructure; 339 | sourceTree = ""; 340 | }; 341 | 6D139F0B2C23C111004DF286 /* Configuration */ = { 342 | isa = PBXGroup; 343 | children = ( 344 | 6D139F062C23BB0F004DF286 /* ConfigurationManager.swift */, 345 | ); 346 | path = Configuration; 347 | sourceTree = ""; 348 | }; 349 | 6D139F0E2C23C33B004DF286 /* Presentation */ = { 350 | isa = PBXGroup; 351 | children = ( 352 | 6D139F1B2C23C47A004DF286 /* WeatherViewModelTests.swift */, 353 | ); 354 | path = Presentation; 355 | sourceTree = ""; 356 | }; 357 | 6D139F0F2C23C349004DF286 /* Domain */ = { 358 | isa = PBXGroup; 359 | children = ( 360 | 6D139F192C23C450004DF286 /* FetchWeatherUseCaseTests.swift */, 361 | ); 362 | path = Domain; 363 | sourceTree = ""; 364 | }; 365 | 6D139F102C23C358004DF286 /* Data */ = { 366 | isa = PBXGroup; 367 | children = ( 368 | 6D139F172C23C42E004DF286 /* WeatherRepositoryTests.swift */, 369 | ); 370 | path = Data; 371 | sourceTree = ""; 372 | }; 373 | 6D139F112C23C363004DF286 /* Infrastructure */ = { 374 | isa = PBXGroup; 375 | children = ( 376 | 6D139F152C23C3E5004DF286 /* APIClientTests.swift */, 377 | ); 378 | path = Infrastructure; 379 | sourceTree = ""; 380 | }; 381 | 6D139F122C23C36F004DF286 /* Core */ = { 382 | isa = PBXGroup; 383 | children = ( 384 | 6D139F132C23C3AE004DF286 /* ConfigurationManagerTests.swift */, 385 | ); 386 | path = Core; 387 | sourceTree = ""; 388 | }; 389 | 6D139F1F2C23C9B0004DF286 /* Utilities */ = { 390 | isa = PBXGroup; 391 | children = ( 392 | 6D139F202C23C9C2004DF286 /* JSONLoader.swift */, 393 | ); 394 | path = Utilities; 395 | sourceTree = ""; 396 | }; 397 | 6D139F222C23CA17004DF286 /* Local */ = { 398 | isa = PBXGroup; 399 | children = ( 400 | 6D139F232C23CA2E004DF286 /* LocalWeatherDataSource.swift */, 401 | ); 402 | path = Local; 403 | sourceTree = ""; 404 | }; 405 | 6D139F252C23CB04004DF286 /* Resources */ = { 406 | isa = PBXGroup; 407 | children = ( 408 | 6D139EB42C23342C004DF286 /* Assets.xcassets */, 409 | 6D139F332C23DA35004DF286 /* weather_data.json */, 410 | ); 411 | path = Resources; 412 | sourceTree = ""; 413 | }; 414 | 6D139F272C23CE3D004DF286 /* Remote */ = { 415 | isa = PBXGroup; 416 | children = ( 417 | 6D139F282C23CE4E004DF286 /* RemoteWeatherDataSource.swift */, 418 | ); 419 | path = Remote; 420 | sourceTree = ""; 421 | }; 422 | 6D139F2C2C23D05A004DF286 /* Interfaces */ = { 423 | isa = PBXGroup; 424 | children = ( 425 | 6D139F2F2C23D226004DF286 /* APIClientProtocol.swift */, 426 | 6D139F2D2C23D06D004DF286 /* JSONLoaderProtocol.swift */, 427 | 6D139F312C23D505004DF286 /* ConfigurationProtocol.swift */, 428 | ); 429 | path = Interfaces; 430 | sourceTree = ""; 431 | }; 432 | /* End PBXGroup section */ 433 | 434 | /* Begin PBXNativeTarget section */ 435 | 6D139EAC2C23342A004DF286 /* ExampleMVVM */ = { 436 | isa = PBXNativeTarget; 437 | buildConfigurationList = 6D139ED12C23342D004DF286 /* Build configuration list for PBXNativeTarget "ExampleMVVM" */; 438 | buildPhases = ( 439 | 6D139EA92C23342A004DF286 /* Sources */, 440 | 6D139EAA2C23342A004DF286 /* Frameworks */, 441 | 6D139EAB2C23342A004DF286 /* Resources */, 442 | ); 443 | buildRules = ( 444 | ); 445 | dependencies = ( 446 | ); 447 | name = ExampleMVVM; 448 | productName = ExampleMVVM; 449 | productReference = 6D139EAD2C23342A004DF286 /* ExampleMVVM.app */; 450 | productType = "com.apple.product-type.application"; 451 | }; 452 | 6D139EBC2C23342C004DF286 /* ExampleMVVMTests */ = { 453 | isa = PBXNativeTarget; 454 | buildConfigurationList = 6D139ED42C23342D004DF286 /* Build configuration list for PBXNativeTarget "ExampleMVVMTests" */; 455 | buildPhases = ( 456 | 6D139EB92C23342C004DF286 /* Sources */, 457 | 6D139EBA2C23342C004DF286 /* Frameworks */, 458 | 6D139EBB2C23342C004DF286 /* Resources */, 459 | ); 460 | buildRules = ( 461 | ); 462 | dependencies = ( 463 | 6D139EBF2C23342C004DF286 /* PBXTargetDependency */, 464 | ); 465 | name = ExampleMVVMTests; 466 | productName = ExampleMVVMTests; 467 | productReference = 6D139EBD2C23342C004DF286 /* ExampleMVVMTests.xctest */; 468 | productType = "com.apple.product-type.bundle.unit-test"; 469 | }; 470 | 6D139EC62C23342D004DF286 /* ExampleMVVMUITests */ = { 471 | isa = PBXNativeTarget; 472 | buildConfigurationList = 6D139ED72C23342D004DF286 /* Build configuration list for PBXNativeTarget "ExampleMVVMUITests" */; 473 | buildPhases = ( 474 | 6D139EC32C23342D004DF286 /* Sources */, 475 | 6D139EC42C23342D004DF286 /* Frameworks */, 476 | 6D139EC52C23342D004DF286 /* Resources */, 477 | ); 478 | buildRules = ( 479 | ); 480 | dependencies = ( 481 | 6D139EC92C23342D004DF286 /* PBXTargetDependency */, 482 | ); 483 | name = ExampleMVVMUITests; 484 | productName = ExampleMVVMUITests; 485 | productReference = 6D139EC72C23342D004DF286 /* ExampleMVVMUITests.xctest */; 486 | productType = "com.apple.product-type.bundle.ui-testing"; 487 | }; 488 | /* End PBXNativeTarget section */ 489 | 490 | /* Begin PBXProject section */ 491 | 6D139EA52C23342A004DF286 /* Project object */ = { 492 | isa = PBXProject; 493 | attributes = { 494 | BuildIndependentTargetsInParallel = 1; 495 | LastSwiftUpdateCheck = 1510; 496 | LastUpgradeCheck = 1510; 497 | TargetAttributes = { 498 | 6D139EAC2C23342A004DF286 = { 499 | CreatedOnToolsVersion = 15.1; 500 | }; 501 | 6D139EBC2C23342C004DF286 = { 502 | CreatedOnToolsVersion = 15.1; 503 | TestTargetID = 6D139EAC2C23342A004DF286; 504 | }; 505 | 6D139EC62C23342D004DF286 = { 506 | CreatedOnToolsVersion = 15.1; 507 | TestTargetID = 6D139EAC2C23342A004DF286; 508 | }; 509 | }; 510 | }; 511 | buildConfigurationList = 6D139EA82C23342A004DF286 /* Build configuration list for PBXProject "ExampleMVVM" */; 512 | compatibilityVersion = "Xcode 14.0"; 513 | developmentRegion = en; 514 | hasScannedForEncodings = 0; 515 | knownRegions = ( 516 | en, 517 | Base, 518 | ); 519 | mainGroup = 6D139EA42C23342A004DF286; 520 | productRefGroup = 6D139EAE2C23342A004DF286 /* Products */; 521 | projectDirPath = ""; 522 | projectRoot = ""; 523 | targets = ( 524 | 6D139EAC2C23342A004DF286 /* ExampleMVVM */, 525 | 6D139EBC2C23342C004DF286 /* ExampleMVVMTests */, 526 | 6D139EC62C23342D004DF286 /* ExampleMVVMUITests */, 527 | ); 528 | }; 529 | /* End PBXProject section */ 530 | 531 | /* Begin PBXResourcesBuildPhase section */ 532 | 6D139EAB2C23342A004DF286 /* Resources */ = { 533 | isa = PBXResourcesBuildPhase; 534 | buildActionMask = 2147483647; 535 | files = ( 536 | 6D139EB82C23342C004DF286 /* Preview Assets.xcassets in Resources */, 537 | 6D139F342C23DA35004DF286 /* weather_data.json in Resources */, 538 | 6D139EB52C23342C004DF286 /* Assets.xcassets in Resources */, 539 | ); 540 | runOnlyForDeploymentPostprocessing = 0; 541 | }; 542 | 6D139EBB2C23342C004DF286 /* Resources */ = { 543 | isa = PBXResourcesBuildPhase; 544 | buildActionMask = 2147483647; 545 | files = ( 546 | ); 547 | runOnlyForDeploymentPostprocessing = 0; 548 | }; 549 | 6D139EC52C23342D004DF286 /* Resources */ = { 550 | isa = PBXResourcesBuildPhase; 551 | buildActionMask = 2147483647; 552 | files = ( 553 | ); 554 | runOnlyForDeploymentPostprocessing = 0; 555 | }; 556 | /* End PBXResourcesBuildPhase section */ 557 | 558 | /* Begin PBXSourcesBuildPhase section */ 559 | 6D139EA92C23342A004DF286 /* Sources */ = { 560 | isa = PBXSourcesBuildPhase; 561 | buildActionMask = 2147483647; 562 | files = ( 563 | 6D139EF62C23380E004DF286 /* WeatherView.swift in Sources */, 564 | 6D139EEB2C23368B004DF286 /* WeatherRepositoryImpl.swift in Sources */, 565 | 6D139EDF2C2334E7004DF286 /* Weather.swift in Sources */, 566 | 6D139EE52C2335C0004DF286 /* City.swift in Sources */, 567 | 6D139F072C23BB0F004DF286 /* ConfigurationManager.swift in Sources */, 568 | 6D139F302C23D226004DF286 /* APIClientProtocol.swift in Sources */, 569 | 6D139EF82C23381D004DF286 /* WeatherRowView.swift in Sources */, 570 | 6D139F2E2C23D06D004DF286 /* JSONLoaderProtocol.swift in Sources */, 571 | 6D139EF42C2337EC004DF286 /* WeatherViewModel.swift in Sources */, 572 | 6D139EE32C233536004DF286 /* WeatherRepository.swift in Sources */, 573 | 6D139EFF2C233952004DF286 /* APIClient.swift in Sources */, 574 | 6D139F292C23CE4E004DF286 /* RemoteWeatherDataSource.swift in Sources */, 575 | 6D139F322C23D505004DF286 /* ConfigurationProtocol.swift in Sources */, 576 | 6D139F2B2C23CECB004DF286 /* WeatherDataSource.swift in Sources */, 577 | 6D139F042C23B8A6004DF286 /* Extension+Date.swift in Sources */, 578 | 6D139EB12C23342A004DF286 /* ExampleMVVMApp.swift in Sources */, 579 | 6D139F092C23BB8D004DF286 /* HTTPMethod.swift in Sources */, 580 | 6D139F242C23CA2E004DF286 /* LocalWeatherDataSource.swift in Sources */, 581 | 6D139F212C23C9C2004DF286 /* JSONLoader.swift in Sources */, 582 | 6D139EE72C2335E4004DF286 /* Temperature.swift in Sources */, 583 | 6D139F022C233D4E004DF286 /* WeatherAPIConfiguration.swift in Sources */, 584 | 6D139EE12C233509004DF286 /* FetchWeatherUseCase.swift in Sources */, 585 | ); 586 | runOnlyForDeploymentPostprocessing = 0; 587 | }; 588 | 6D139EB92C23342C004DF286 /* Sources */ = { 589 | isa = PBXSourcesBuildPhase; 590 | buildActionMask = 2147483647; 591 | files = ( 592 | 6D139F1A2C23C450004DF286 /* FetchWeatherUseCaseTests.swift in Sources */, 593 | 6D139F162C23C3E5004DF286 /* APIClientTests.swift in Sources */, 594 | 6D139F142C23C3AE004DF286 /* ConfigurationManagerTests.swift in Sources */, 595 | 6D139F182C23C42E004DF286 /* WeatherRepositoryTests.swift in Sources */, 596 | 6D139EC22C23342C004DF286 /* ExampleMVVMTests.swift in Sources */, 597 | 6D139F1C2C23C47A004DF286 /* WeatherViewModelTests.swift in Sources */, 598 | ); 599 | runOnlyForDeploymentPostprocessing = 0; 600 | }; 601 | 6D139EC32C23342D004DF286 /* Sources */ = { 602 | isa = PBXSourcesBuildPhase; 603 | buildActionMask = 2147483647; 604 | files = ( 605 | 6D139ECC2C23342D004DF286 /* ExampleMVVMUITests.swift in Sources */, 606 | 6D139ECE2C23342D004DF286 /* ExampleMVVMUITestsLaunchTests.swift in Sources */, 607 | ); 608 | runOnlyForDeploymentPostprocessing = 0; 609 | }; 610 | /* End PBXSourcesBuildPhase section */ 611 | 612 | /* Begin PBXTargetDependency section */ 613 | 6D139EBF2C23342C004DF286 /* PBXTargetDependency */ = { 614 | isa = PBXTargetDependency; 615 | target = 6D139EAC2C23342A004DF286 /* ExampleMVVM */; 616 | targetProxy = 6D139EBE2C23342C004DF286 /* PBXContainerItemProxy */; 617 | }; 618 | 6D139EC92C23342D004DF286 /* PBXTargetDependency */ = { 619 | isa = PBXTargetDependency; 620 | target = 6D139EAC2C23342A004DF286 /* ExampleMVVM */; 621 | targetProxy = 6D139EC82C23342D004DF286 /* PBXContainerItemProxy */; 622 | }; 623 | /* End PBXTargetDependency section */ 624 | 625 | /* Begin XCBuildConfiguration section */ 626 | 6D139ECF2C23342D004DF286 /* Debug */ = { 627 | isa = XCBuildConfiguration; 628 | buildSettings = { 629 | ALWAYS_SEARCH_USER_PATHS = NO; 630 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 631 | CLANG_ANALYZER_NONNULL = YES; 632 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 633 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 634 | CLANG_ENABLE_MODULES = YES; 635 | CLANG_ENABLE_OBJC_ARC = YES; 636 | CLANG_ENABLE_OBJC_WEAK = YES; 637 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 638 | CLANG_WARN_BOOL_CONVERSION = YES; 639 | CLANG_WARN_COMMA = YES; 640 | CLANG_WARN_CONSTANT_CONVERSION = YES; 641 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_NON_LITERAL_NULL_CONVERSION = YES; 649 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 650 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 651 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 652 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 653 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 654 | CLANG_WARN_STRICT_PROTOTYPES = YES; 655 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 656 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 657 | CLANG_WARN_UNREACHABLE_CODE = YES; 658 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 659 | COPY_PHASE_STRIP = NO; 660 | DEBUG_INFORMATION_FORMAT = dwarf; 661 | ENABLE_STRICT_OBJC_MSGSEND = YES; 662 | ENABLE_TESTABILITY = YES; 663 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 664 | GCC_C_LANGUAGE_STANDARD = gnu17; 665 | GCC_DYNAMIC_NO_PIC = NO; 666 | GCC_NO_COMMON_BLOCKS = YES; 667 | GCC_OPTIMIZATION_LEVEL = 0; 668 | GCC_PREPROCESSOR_DEFINITIONS = ( 669 | "DEBUG=1", 670 | "$(inherited)", 671 | ); 672 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 673 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 674 | GCC_WARN_UNDECLARED_SELECTOR = YES; 675 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 676 | GCC_WARN_UNUSED_FUNCTION = YES; 677 | GCC_WARN_UNUSED_VARIABLE = YES; 678 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 679 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 680 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 681 | MTL_FAST_MATH = YES; 682 | ONLY_ACTIVE_ARCH = YES; 683 | SDKROOT = iphoneos; 684 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 685 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 686 | }; 687 | name = Debug; 688 | }; 689 | 6D139ED02C23342D004DF286 /* Release */ = { 690 | isa = XCBuildConfiguration; 691 | buildSettings = { 692 | ALWAYS_SEARCH_USER_PATHS = NO; 693 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 694 | CLANG_ANALYZER_NONNULL = YES; 695 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 696 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 697 | CLANG_ENABLE_MODULES = YES; 698 | CLANG_ENABLE_OBJC_ARC = YES; 699 | CLANG_ENABLE_OBJC_WEAK = YES; 700 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 701 | CLANG_WARN_BOOL_CONVERSION = YES; 702 | CLANG_WARN_COMMA = YES; 703 | CLANG_WARN_CONSTANT_CONVERSION = YES; 704 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 705 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 706 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 707 | CLANG_WARN_EMPTY_BODY = YES; 708 | CLANG_WARN_ENUM_CONVERSION = YES; 709 | CLANG_WARN_INFINITE_RECURSION = YES; 710 | CLANG_WARN_INT_CONVERSION = YES; 711 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 712 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 713 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 714 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 715 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 716 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 717 | CLANG_WARN_STRICT_PROTOTYPES = YES; 718 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 719 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 720 | CLANG_WARN_UNREACHABLE_CODE = YES; 721 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 722 | COPY_PHASE_STRIP = NO; 723 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 724 | ENABLE_NS_ASSERTIONS = NO; 725 | ENABLE_STRICT_OBJC_MSGSEND = YES; 726 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 727 | GCC_C_LANGUAGE_STANDARD = gnu17; 728 | GCC_NO_COMMON_BLOCKS = YES; 729 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 730 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 731 | GCC_WARN_UNDECLARED_SELECTOR = YES; 732 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 733 | GCC_WARN_UNUSED_FUNCTION = YES; 734 | GCC_WARN_UNUSED_VARIABLE = YES; 735 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 736 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 737 | MTL_ENABLE_DEBUG_INFO = NO; 738 | MTL_FAST_MATH = YES; 739 | SDKROOT = iphoneos; 740 | SWIFT_COMPILATION_MODE = wholemodule; 741 | VALIDATE_PRODUCT = YES; 742 | }; 743 | name = Release; 744 | }; 745 | 6D139ED22C23342D004DF286 /* Debug */ = { 746 | isa = XCBuildConfiguration; 747 | buildSettings = { 748 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 749 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 750 | CODE_SIGN_STYLE = Automatic; 751 | CURRENT_PROJECT_VERSION = 1; 752 | DEVELOPMENT_ASSET_PATHS = "\"ExampleMVVM/Preview Content\""; 753 | DEVELOPMENT_TEAM = 3LBDBT47F7; 754 | ENABLE_PREVIEWS = YES; 755 | GENERATE_INFOPLIST_FILE = YES; 756 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 757 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 758 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 759 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 760 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 761 | LD_RUNPATH_SEARCH_PATHS = ( 762 | "$(inherited)", 763 | "@executable_path/Frameworks", 764 | ); 765 | MARKETING_VERSION = 1.0; 766 | PRODUCT_BUNDLE_IDENTIFIER = clean.nazmulkp.ExampleMVVM; 767 | PRODUCT_NAME = "$(TARGET_NAME)"; 768 | SWIFT_EMIT_LOC_STRINGS = YES; 769 | SWIFT_VERSION = 5.0; 770 | TARGETED_DEVICE_FAMILY = "1,2"; 771 | }; 772 | name = Debug; 773 | }; 774 | 6D139ED32C23342D004DF286 /* Release */ = { 775 | isa = XCBuildConfiguration; 776 | buildSettings = { 777 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 778 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 779 | CODE_SIGN_STYLE = Automatic; 780 | CURRENT_PROJECT_VERSION = 1; 781 | DEVELOPMENT_ASSET_PATHS = "\"ExampleMVVM/Preview Content\""; 782 | DEVELOPMENT_TEAM = 3LBDBT47F7; 783 | ENABLE_PREVIEWS = YES; 784 | GENERATE_INFOPLIST_FILE = YES; 785 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 786 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 787 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 788 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 789 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 790 | LD_RUNPATH_SEARCH_PATHS = ( 791 | "$(inherited)", 792 | "@executable_path/Frameworks", 793 | ); 794 | MARKETING_VERSION = 1.0; 795 | PRODUCT_BUNDLE_IDENTIFIER = clean.nazmulkp.ExampleMVVM; 796 | PRODUCT_NAME = "$(TARGET_NAME)"; 797 | SWIFT_EMIT_LOC_STRINGS = YES; 798 | SWIFT_VERSION = 5.0; 799 | TARGETED_DEVICE_FAMILY = "1,2"; 800 | }; 801 | name = Release; 802 | }; 803 | 6D139ED52C23342D004DF286 /* Debug */ = { 804 | isa = XCBuildConfiguration; 805 | buildSettings = { 806 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 807 | BUNDLE_LOADER = "$(TEST_HOST)"; 808 | CODE_SIGN_STYLE = Automatic; 809 | CURRENT_PROJECT_VERSION = 1; 810 | DEVELOPMENT_TEAM = 3LBDBT47F7; 811 | GENERATE_INFOPLIST_FILE = YES; 812 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 813 | MARKETING_VERSION = 1.0; 814 | PRODUCT_BUNDLE_IDENTIFIER = clean.nazmulkp.ExampleMVVMTests; 815 | PRODUCT_NAME = "$(TARGET_NAME)"; 816 | SWIFT_EMIT_LOC_STRINGS = NO; 817 | SWIFT_VERSION = 5.0; 818 | TARGETED_DEVICE_FAMILY = "1,2"; 819 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ExampleMVVM.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ExampleMVVM"; 820 | }; 821 | name = Debug; 822 | }; 823 | 6D139ED62C23342D004DF286 /* Release */ = { 824 | isa = XCBuildConfiguration; 825 | buildSettings = { 826 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 827 | BUNDLE_LOADER = "$(TEST_HOST)"; 828 | CODE_SIGN_STYLE = Automatic; 829 | CURRENT_PROJECT_VERSION = 1; 830 | DEVELOPMENT_TEAM = 3LBDBT47F7; 831 | GENERATE_INFOPLIST_FILE = YES; 832 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 833 | MARKETING_VERSION = 1.0; 834 | PRODUCT_BUNDLE_IDENTIFIER = clean.nazmulkp.ExampleMVVMTests; 835 | PRODUCT_NAME = "$(TARGET_NAME)"; 836 | SWIFT_EMIT_LOC_STRINGS = NO; 837 | SWIFT_VERSION = 5.0; 838 | TARGETED_DEVICE_FAMILY = "1,2"; 839 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ExampleMVVM.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ExampleMVVM"; 840 | }; 841 | name = Release; 842 | }; 843 | 6D139ED82C23342D004DF286 /* Debug */ = { 844 | isa = XCBuildConfiguration; 845 | buildSettings = { 846 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 847 | CODE_SIGN_STYLE = Automatic; 848 | CURRENT_PROJECT_VERSION = 1; 849 | DEVELOPMENT_TEAM = 3LBDBT47F7; 850 | GENERATE_INFOPLIST_FILE = YES; 851 | MARKETING_VERSION = 1.0; 852 | PRODUCT_BUNDLE_IDENTIFIER = clean.nazmulkp.ExampleMVVMUITests; 853 | PRODUCT_NAME = "$(TARGET_NAME)"; 854 | SWIFT_EMIT_LOC_STRINGS = NO; 855 | SWIFT_VERSION = 5.0; 856 | TARGETED_DEVICE_FAMILY = "1,2"; 857 | TEST_TARGET_NAME = ExampleMVVM; 858 | }; 859 | name = Debug; 860 | }; 861 | 6D139ED92C23342D004DF286 /* Release */ = { 862 | isa = XCBuildConfiguration; 863 | buildSettings = { 864 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 865 | CODE_SIGN_STYLE = Automatic; 866 | CURRENT_PROJECT_VERSION = 1; 867 | DEVELOPMENT_TEAM = 3LBDBT47F7; 868 | GENERATE_INFOPLIST_FILE = YES; 869 | MARKETING_VERSION = 1.0; 870 | PRODUCT_BUNDLE_IDENTIFIER = clean.nazmulkp.ExampleMVVMUITests; 871 | PRODUCT_NAME = "$(TARGET_NAME)"; 872 | SWIFT_EMIT_LOC_STRINGS = NO; 873 | SWIFT_VERSION = 5.0; 874 | TARGETED_DEVICE_FAMILY = "1,2"; 875 | TEST_TARGET_NAME = ExampleMVVM; 876 | }; 877 | name = Release; 878 | }; 879 | /* End XCBuildConfiguration section */ 880 | 881 | /* Begin XCConfigurationList section */ 882 | 6D139EA82C23342A004DF286 /* Build configuration list for PBXProject "ExampleMVVM" */ = { 883 | isa = XCConfigurationList; 884 | buildConfigurations = ( 885 | 6D139ECF2C23342D004DF286 /* Debug */, 886 | 6D139ED02C23342D004DF286 /* Release */, 887 | ); 888 | defaultConfigurationIsVisible = 0; 889 | defaultConfigurationName = Release; 890 | }; 891 | 6D139ED12C23342D004DF286 /* Build configuration list for PBXNativeTarget "ExampleMVVM" */ = { 892 | isa = XCConfigurationList; 893 | buildConfigurations = ( 894 | 6D139ED22C23342D004DF286 /* Debug */, 895 | 6D139ED32C23342D004DF286 /* Release */, 896 | ); 897 | defaultConfigurationIsVisible = 0; 898 | defaultConfigurationName = Release; 899 | }; 900 | 6D139ED42C23342D004DF286 /* Build configuration list for PBXNativeTarget "ExampleMVVMTests" */ = { 901 | isa = XCConfigurationList; 902 | buildConfigurations = ( 903 | 6D139ED52C23342D004DF286 /* Debug */, 904 | 6D139ED62C23342D004DF286 /* Release */, 905 | ); 906 | defaultConfigurationIsVisible = 0; 907 | defaultConfigurationName = Release; 908 | }; 909 | 6D139ED72C23342D004DF286 /* Build configuration list for PBXNativeTarget "ExampleMVVMUITests" */ = { 910 | isa = XCConfigurationList; 911 | buildConfigurations = ( 912 | 6D139ED82C23342D004DF286 /* Debug */, 913 | 6D139ED92C23342D004DF286 /* Release */, 914 | ); 915 | defaultConfigurationIsVisible = 0; 916 | defaultConfigurationName = Release; 917 | }; 918 | /* End XCConfigurationList section */ 919 | }; 920 | rootObject = 6D139EA52C23342A004DF286 /* Project object */; 921 | } 922 | -------------------------------------------------------------------------------- /ExampleMVVM.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ExampleMVVM.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ExampleMVVM.xcodeproj/project.xcworkspace/xcuserdata/macbookairm1.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazmulkp/Clean-Architecture-Swiftui-MVVM/d49a9a69867b0465393713cd2ffbc7b456e8eacf/ExampleMVVM.xcodeproj/project.xcworkspace/xcuserdata/macbookairm1.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ExampleMVVM.xcodeproj/xcuserdata/macbookairm1.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 36 | 37 | 51 | 52 | 53 | 54 | 55 | 57 | 69 | 70 | 84 | 85 | 99 | 100 | 101 | 102 | 103 | 105 | 117 | 118 | 132 | 133 | 147 | 148 | 149 | 150 | 151 | 153 | 165 | 166 | 167 | 169 | 181 | 182 | 183 | 185 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /ExampleMVVM.xcodeproj/xcuserdata/macbookairm1.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ExampleMVVM.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ExampleMVVM/App/ExampleMVVMApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleMVVMApp.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ExampleMVVMApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | let apiClient = APIClient() 15 | let appConfiguration = AppConfiguration() 16 | let apiConfiguration = WeatherAPIConfiguration(configuration: appConfiguration) 17 | let remoteDataSource = RemoteWeatherDataSource(apiClient: apiClient, apiConfiguration: apiConfiguration) 18 | 19 | let jsonLoader = JSONLoader() 20 | let localDataSource = LocalWeatherDataSource(jsonLoader: jsonLoader, configuration: appConfiguration) 21 | 22 | // Inject the appropriate data source 23 | let useLocalData = true // Change this to true to use local data 24 | let dataSource: WeatherDataSource = useLocalData ? localDataSource : remoteDataSource 25 | 26 | let repository = WeatherRepositoryImpl(dataSource: dataSource) 27 | let fetchWeatherUseCase = FetchWeatherUseCase(repository: repository) 28 | let viewModel = WeatherViewModel(fetchWeatherUseCase: fetchWeatherUseCase) 29 | 30 | WeatherView(viewModel: viewModel) 31 | } 32 | } 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /ExampleMVVM/Core/Extensions/Extension+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extension+Date.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DateFormatter { 11 | static var shortDate: DateFormatter { 12 | let formatter = DateFormatter() 13 | formatter.dateStyle = .short 14 | return formatter 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ExampleMVVM/Data/Local/LocalWeatherDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalWeatherDataSource.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public class LocalWeatherDataSource: WeatherDataSource { 11 | private let jsonLoader: JSONLoaderProtocol 12 | private let configuration: ConfigurationProtocol 13 | 14 | public init(jsonLoader: JSONLoaderProtocol, configuration: ConfigurationProtocol) { 15 | self.jsonLoader = jsonLoader 16 | self.configuration = configuration 17 | } 18 | 19 | public func fetchWeather(for city: String, completion: @escaping (Result) -> Void) { 20 | let filename = configuration.localWeatherDataFilename 21 | if let data = jsonLoader.loadJSON(filename: filename) { 22 | do { 23 | let forecast = try JSONDecoder().decode(Forecast.self, from: data) 24 | print(forecast) 25 | completion(.success(forecast)) 26 | } catch { 27 | completion(.failure(error)) 28 | } 29 | } else { 30 | completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to load local data"]))) 31 | } 32 | } 33 | } 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ExampleMVVM/Data/Remote/RemoteWeatherDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteWeatherDataSource.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | import Foundation 11 | 12 | public class RemoteWeatherDataSource: WeatherDataSource { 13 | private let apiClient: APIClientProtocol 14 | private let apiConfiguration: WeatherAPIConfiguration 15 | 16 | public init(apiClient: APIClientProtocol, apiConfiguration: WeatherAPIConfiguration) { 17 | self.apiClient = apiClient 18 | self.apiConfiguration = apiConfiguration 19 | } 20 | 21 | public func fetchWeather(for city: String, completion: @escaping (Result) -> Void) { 22 | guard let url = apiConfiguration.makeWeatherURL(for: city) else { 23 | completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))) 24 | return 25 | } 26 | 27 | apiClient.request(url: url, method: .get, completion: completion) 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /ExampleMVVM/Data/Repositories/WeatherRepositoryImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherRepositoryImpl.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public class WeatherRepositoryImpl: WeatherRepository { 11 | private let dataSource: WeatherDataSource 12 | 13 | public init(dataSource: WeatherDataSource) { 14 | self.dataSource = dataSource 15 | } 16 | 17 | public func fetchWeather(for city: String, completion: @escaping (Result) -> Void) { 18 | dataSource.fetchWeather(for: city, completion: completion) 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /ExampleMVVM/Domain/Entities/City.swift: -------------------------------------------------------------------------------- 1 | // 2 | // City.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct City: Equatable, Codable { 11 | 12 | // MARK: - Parameters 13 | 14 | public var name: String 15 | public var country: String 16 | } 17 | -------------------------------------------------------------------------------- /ExampleMVVM/Domain/Entities/Temperature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Temperature.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Temperature: Codable, Equatable { 11 | 12 | // MARK: - Properties 13 | 14 | public let current: Double 15 | public let min: Double 16 | public let max: Double 17 | 18 | // MARK: - Codable 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case current = "temp" 22 | case min = "temp_min" 23 | case max = "temp_max" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ExampleMVVM/Domain/Entities/Weather.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Weather.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Forecast: Codable, Equatable { 11 | 12 | // MARK: - Properties 13 | 14 | public var city: City 15 | public var weatherBundle: [Weather] 16 | 17 | // MARK: - Codable 18 | 19 | enum CodingKeys: String, CodingKey { 20 | case city 21 | case weatherBundle = "list" 22 | } 23 | } 24 | 25 | 26 | public struct Weather: Codable, Equatable { 27 | 28 | // MARK: - Properties 29 | 30 | public let dateTime: Double 31 | public let data: [WeatherData] 32 | public let temperature: Temperature 33 | 34 | // MARK: - Convenience Properties 35 | 36 | public var type: WeatherType { 37 | guard let name = data.first?.name else { return .sunny } 38 | return WeatherType(rawValue: name) ?? .sunny 39 | } 40 | 41 | // MARK: - CodingKeys 42 | 43 | enum CodingKeys: String, CodingKey { 44 | case dateTime = "dt" 45 | case data = "weather" 46 | case temperature = "main" 47 | } 48 | } 49 | 50 | public struct WeatherData: Codable, Equatable { 51 | 52 | // MARK: - Properties 53 | 54 | public let name: String 55 | 56 | // MARK: - Codable 57 | 58 | enum CodingKeys: String, CodingKey { 59 | case name = "main" 60 | } 61 | } 62 | 63 | public enum WeatherType: String { 64 | case sunny = "Clear" 65 | case cloudy = "Clouds" 66 | case rainy = "Rain" 67 | } 68 | -------------------------------------------------------------------------------- /ExampleMVVM/Domain/Interfaces/WeatherDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherDataSource.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol WeatherDataSource { 11 | func fetchWeather(for city: String, completion: @escaping (Result) -> Void) 12 | } 13 | 14 | -------------------------------------------------------------------------------- /ExampleMVVM/Domain/Interfaces/WeatherRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherRepository.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public protocol WeatherRepository { 11 | func fetchWeather(for city: String, completion: @escaping (Result) -> Void) 12 | } 13 | -------------------------------------------------------------------------------- /ExampleMVVM/Domain/UseCases/FetchWeatherUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchWeatherUseCase.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public class FetchWeatherUseCase { 11 | private let repository: WeatherRepository 12 | 13 | public init(repository: WeatherRepository) { 14 | self.repository = repository 15 | } 16 | 17 | public func execute(city: String, completion: @escaping (Result) -> Void) { 18 | repository.fetchWeather(for: city, completion: completion) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Interfaces/APIClientProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClientProtocol.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol APIClientProtocol { 11 | func request(url: URL, method: HTTPMethod, completion: @escaping (Result) -> Void) 12 | } 13 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Interfaces/ConfigurationProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationProtocol.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ConfigurationProtocol { 11 | var baseURL: String { get } 12 | var apiKey: String { get } 13 | var localWeatherDataFilename: String { get } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Interfaces/JSONLoaderProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONLoaderProtocol.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol JSONLoaderProtocol { 11 | func loadJSON(filename: String) -> Data? 12 | } 13 | 14 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Network/APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class APIClient: APIClientProtocol { 11 | private let session: URLSession 12 | 13 | init(session: URLSession = .shared) { 14 | self.session = session 15 | } 16 | 17 | func request(url: URL, method: HTTPMethod, completion: @escaping (Result) -> Void) { 18 | var request = URLRequest(url: url) 19 | request.httpMethod = method.rawValue 20 | 21 | let task = session.dataTask(with: request) { data, response, error in 22 | if let error = error { 23 | completion(.failure(error)) 24 | return 25 | } 26 | 27 | guard let data = data else { 28 | completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) 29 | return 30 | } 31 | 32 | do { 33 | let decodedResponse = try JSONDecoder().decode(T.self, from: data) 34 | completion(.success(decodedResponse)) 35 | } catch { 36 | completion(.failure(error)) 37 | } 38 | } 39 | task.resume() 40 | } 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Network/Configuration/ConfigurationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppConfiguration.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class AppConfiguration: ConfigurationProtocol { 11 | 12 | var baseURL: String { 13 | return "https://api.openweathermap.org/data/2.5/forecast" 14 | } 15 | 16 | var apiKey: String { 17 | return "c8bea7fd8fb47ad823162954a2965e4b" // Replace with your actual API key 18 | } 19 | 20 | var localWeatherDataFilename: String { 21 | return "weather_data" 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Network/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum HTTPMethod: String { 11 | case get = "GET" 12 | case post = "POST" 13 | case put = "PUT" 14 | case delete = "DELETE" 15 | } 16 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Network/WeatherAPIConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherAPIConfiguration.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public class WeatherAPIConfiguration { 11 | private let configuration: ConfigurationProtocol 12 | 13 | init(configuration: ConfigurationProtocol) { 14 | self.configuration = configuration 15 | } 16 | 17 | func makeWeatherURL(for city: String) -> URL? { 18 | let baseURL = configuration.baseURL 19 | let apiKey = configuration.apiKey 20 | 21 | var components = URLComponents(string: baseURL) 22 | components?.queryItems = [ 23 | URLQueryItem(name: "q", value: city), 24 | URLQueryItem(name: "appid", value: apiKey) 25 | ] 26 | return components?.url 27 | } 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /ExampleMVVM/Infrastructure/Utilities/JSONLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONLoader.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class JSONLoader: JSONLoaderProtocol { 11 | func loadJSON(filename: String) -> Data? { 12 | if let url = Bundle.main.url(forResource: filename, withExtension: "json") { 13 | do { 14 | return try Data(contentsOf: url) 15 | } catch { 16 | print("Error loading local JSON file: \(error)") 17 | } 18 | } 19 | return nil 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /ExampleMVVM/Presentation/Weather/View/WeatherRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherRowView.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WeatherRowView: View { 11 | let weather: Weather 12 | 13 | var body: some View { 14 | VStack(alignment: .leading) { 15 | Text("Date: \(Date(timeIntervalSince1970: weather.dateTime), formatter: DateFormatter.shortDate)") 16 | Text("Temperature: \(weather.temperature.current)°C") 17 | Text("Condition: \(weather.type.rawValue)") 18 | } 19 | .padding() 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /ExampleMVVM/Presentation/Weather/View/WeatherView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherView.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WeatherView: View { 11 | @StateObject private var viewModel: WeatherViewModel 12 | 13 | init(viewModel: WeatherViewModel) { 14 | _viewModel = StateObject(wrappedValue: viewModel) 15 | } 16 | 17 | @State private var city: String = "" 18 | 19 | var body: some View { 20 | NavigationView { 21 | VStack { 22 | TextField("Enter city", text: $city, onCommit: { 23 | viewModel.fetchWeather(for: city) 24 | }) 25 | .textFieldStyle(RoundedBorderTextFieldStyle()) 26 | .padding() 27 | 28 | if let forecast = viewModel.forecast { 29 | List { 30 | ForEach(forecast.weatherBundle, id: \.dateTime) { weather in 31 | WeatherRowView(weather: weather) 32 | } 33 | } 34 | } else if let errorMessage = viewModel.errorMessage { 35 | Text(errorMessage) 36 | .foregroundColor(.red) 37 | } else { 38 | Text("Enter a city to get the weather forecast") 39 | .foregroundColor(.gray) 40 | } 41 | } 42 | .navigationTitle("Weather") 43 | } 44 | } 45 | } 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ExampleMVVM/Presentation/Weather/ViewModel/WeatherViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherViewModel.swift 3 | // ExampleMVVM 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | public class WeatherViewModel: ObservableObject { 12 | private let fetchWeatherUseCase: FetchWeatherUseCase 13 | @Published var forecast: Forecast? 14 | @Published var errorMessage: String? 15 | 16 | public init(fetchWeatherUseCase: FetchWeatherUseCase) { 17 | self.fetchWeatherUseCase = fetchWeatherUseCase 18 | } 19 | 20 | public func fetchWeather(for city: String) { 21 | fetchWeatherUseCase.execute(city: city) { [weak self] result in 22 | switch result { 23 | case .success(let forecast): 24 | DispatchQueue.main.async { 25 | self?.forecast = forecast 26 | } 27 | case .failure(let error): 28 | DispatchQueue.main.async { 29 | self?.errorMessage = error.localizedDescription 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ExampleMVVM/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ExampleMVVM/Resources/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 | -------------------------------------------------------------------------------- /ExampleMVVM/Resources/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 | -------------------------------------------------------------------------------- /ExampleMVVM/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ExampleMVVM/Resources/weather_data.json: -------------------------------------------------------------------------------- 1 | {"cod":"200","message":0,"cnt":40,"list":[{"dt":1718863200,"main":{"temp":283.37,"feels_like":282.64,"temp_min":283.37,"temp_max":285.64,"pressure":1022,"sea_level":1022,"grnd_level":1019,"humidity":84,"temp_kf":-2.27},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":52},"wind":{"speed":2.25,"deg":55,"gust":3.92},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-20 06:00:00"},{"dt":1718874000,"main":{"temp":288.12,"feels_like":287.27,"temp_min":288.12,"temp_max":291.06,"pressure":1021,"sea_level":1021,"grnd_level":1018,"humidity":61,"temp_kf":-2.94},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":{"all":20},"wind":{"speed":2.38,"deg":68,"gust":2.85},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-20 09:00:00"},{"dt":1718884800,"main":{"temp":294.85,"feels_like":293.99,"temp_min":294.85,"temp_max":294.85,"pressure":1020,"sea_level":1020,"grnd_level":1017,"humidity":35,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":2.43,"deg":69,"gust":1.92},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-20 12:00:00"},{"dt":1718895600,"main":{"temp":296.09,"feels_like":295.35,"temp_min":296.09,"temp_max":296.09,"pressure":1018,"sea_level":1018,"grnd_level":1015,"humidity":35,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":1},"wind":{"speed":2.38,"deg":74,"gust":1.93},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-20 15:00:00"},{"dt":1718906400,"main":{"temp":293.15,"feels_like":292.54,"temp_min":293.15,"temp_max":293.15,"pressure":1018,"sea_level":1018,"grnd_level":1015,"humidity":51,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":2},"wind":{"speed":3.62,"deg":97,"gust":3.38},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-20 18:00:00"},{"dt":1718917200,"main":{"temp":289.12,"feels_like":288.42,"temp_min":289.12,"temp_max":289.12,"pressure":1019,"sea_level":1019,"grnd_level":1015,"humidity":63,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":2},"wind":{"speed":1.44,"deg":115,"gust":3.03},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-20 21:00:00"},{"dt":1718928000,"main":{"temp":287.4,"feels_like":286.63,"temp_min":287.4,"temp_max":287.4,"pressure":1017,"sea_level":1017,"grnd_level":1014,"humidity":67,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":1},"wind":{"speed":0.61,"deg":181,"gust":0.63},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-21 00:00:00"},{"dt":1718938800,"main":{"temp":286.34,"feels_like":285.67,"temp_min":286.34,"temp_max":286.34,"pressure":1016,"sea_level":1016,"grnd_level":1013,"humidity":75,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":3},"wind":{"speed":1.17,"deg":186,"gust":1.26},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-21 03:00:00"},{"dt":1718949600,"main":{"temp":288.22,"feels_like":287.69,"temp_min":288.22,"temp_max":288.22,"pressure":1016,"sea_level":1016,"grnd_level":1013,"humidity":73,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":1},"wind":{"speed":1.3,"deg":222,"gust":2.06},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-21 06:00:00"},{"dt":1718960400,"main":{"temp":293.46,"feels_like":292.93,"temp_min":293.46,"temp_max":293.46,"pressure":1015,"sea_level":1015,"grnd_level":1012,"humidity":53,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":{"all":17},"wind":{"speed":2.23,"deg":248,"gust":3.06},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-21 09:00:00"},{"dt":1718971200,"main":{"temp":297.05,"feels_like":296.57,"temp_min":297.05,"temp_max":297.05,"pressure":1013,"sea_level":1013,"grnd_level":1010,"humidity":41,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":{"all":14},"wind":{"speed":3.61,"deg":225,"gust":4.66},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-21 12:00:00"},{"dt":1718982000,"main":{"temp":296.83,"feels_like":296.38,"temp_min":296.83,"temp_max":296.83,"pressure":1012,"sea_level":1012,"grnd_level":1009,"humidity":43,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":76},"wind":{"speed":5.69,"deg":221,"gust":5.7},"visibility":10000,"pop":0.09,"sys":{"pod":"d"},"dt_txt":"2024-06-21 15:00:00"},{"dt":1718992800,"main":{"temp":293.77,"feels_like":293.22,"temp_min":293.77,"temp_max":293.77,"pressure":1012,"sea_level":1012,"grnd_level":1009,"humidity":51,"temp_kf":0},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":{"all":83},"wind":{"speed":5.47,"deg":237,"gust":6.74},"visibility":10000,"pop":0.06,"sys":{"pod":"d"},"dt_txt":"2024-06-21 18:00:00"},{"dt":1719003600,"main":{"temp":289.9,"feels_like":289.38,"temp_min":289.9,"temp_max":289.9,"pressure":1013,"sea_level":1013,"grnd_level":1010,"humidity":67,"temp_kf":0},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"clouds":{"all":100},"wind":{"speed":4.21,"deg":235,"gust":8.16},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-21 21:00:00"},{"dt":1719014400,"main":{"temp":288.64,"feels_like":287.94,"temp_min":288.64,"temp_max":288.64,"pressure":1013,"sea_level":1013,"grnd_level":1010,"humidity":65,"temp_kf":0},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"clouds":{"all":100},"wind":{"speed":3.76,"deg":229,"gust":8.47},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-22 00:00:00"},{"dt":1719025200,"main":{"temp":287.46,"feels_like":286.9,"temp_min":287.46,"temp_max":287.46,"pressure":1013,"sea_level":1013,"grnd_level":1010,"humidity":75,"temp_kf":0},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"clouds":{"all":100},"wind":{"speed":3.47,"deg":219,"gust":7.01},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-22 03:00:00"},{"dt":1719036000,"main":{"temp":287.27,"feels_like":286.9,"temp_min":287.27,"temp_max":287.27,"pressure":1014,"sea_level":1014,"grnd_level":1011,"humidity":83,"temp_kf":0},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"clouds":{"all":100},"wind":{"speed":3.18,"deg":207,"gust":6.97},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-22 06:00:00"},{"dt":1719046800,"main":{"temp":291.03,"feels_like":290.65,"temp_min":291.03,"temp_max":291.03,"pressure":1014,"sea_level":1014,"grnd_level":1011,"humidity":68,"temp_kf":0},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"clouds":{"all":92},"wind":{"speed":2.03,"deg":213,"gust":3.22},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-22 09:00:00"},{"dt":1719057600,"main":{"temp":293.19,"feels_like":292.76,"temp_min":293.19,"temp_max":293.19,"pressure":1015,"sea_level":1015,"grnd_level":1012,"humidity":58,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":93},"wind":{"speed":1.72,"deg":233,"gust":2.72},"visibility":10000,"pop":0.24,"rain":{"3h":0.22},"sys":{"pod":"d"},"dt_txt":"2024-06-22 12:00:00"},{"dt":1719068400,"main":{"temp":295.82,"feels_like":295.42,"temp_min":295.82,"temp_max":295.82,"pressure":1015,"sea_level":1015,"grnd_level":1012,"humidity":49,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":90},"wind":{"speed":2.11,"deg":255,"gust":3.12},"visibility":10000,"pop":0.78,"rain":{"3h":0.37},"sys":{"pod":"d"},"dt_txt":"2024-06-22 15:00:00"},{"dt":1719079200,"main":{"temp":294.43,"feels_like":294.05,"temp_min":294.43,"temp_max":294.43,"pressure":1016,"sea_level":1016,"grnd_level":1013,"humidity":55,"temp_kf":0},"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"clouds":{"all":78},"wind":{"speed":3.66,"deg":287,"gust":4.06},"visibility":10000,"pop":0.8,"rain":{"3h":0.26},"sys":{"pod":"d"},"dt_txt":"2024-06-22 18:00:00"},{"dt":1719090000,"main":{"temp":290.33,"feels_like":290.06,"temp_min":290.33,"temp_max":290.33,"pressure":1018,"sea_level":1018,"grnd_level":1015,"humidity":75,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":15},"wind":{"speed":2.7,"deg":284,"gust":5.91},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-22 21:00:00"},{"dt":1719100800,"main":{"temp":287.87,"feels_like":287.43,"temp_min":287.87,"temp_max":287.87,"pressure":1019,"sea_level":1019,"grnd_level":1016,"humidity":78,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":11},"wind":{"speed":2.19,"deg":265,"gust":5.04},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-23 00:00:00"},{"dt":1719111600,"main":{"temp":285.89,"feels_like":285.31,"temp_min":285.89,"temp_max":285.89,"pressure":1020,"sea_level":1020,"grnd_level":1016,"humidity":80,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":4},"wind":{"speed":1.82,"deg":244,"gust":4.23},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-23 03:00:00"},{"dt":1719122400,"main":{"temp":287.37,"feels_like":286.68,"temp_min":287.37,"temp_max":287.37,"pressure":1021,"sea_level":1021,"grnd_level":1017,"humidity":70,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":4},"wind":{"speed":2,"deg":251,"gust":2.98},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-23 06:00:00"},{"dt":1719133200,"main":{"temp":292.51,"feels_like":291.62,"temp_min":292.51,"temp_max":292.51,"pressure":1021,"sea_level":1021,"grnd_level":1018,"humidity":43,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":2.47,"deg":252,"gust":3.05},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-23 09:00:00"},{"dt":1719144000,"main":{"temp":296.57,"feels_like":295.86,"temp_min":296.57,"temp_max":296.57,"pressure":1021,"sea_level":1021,"grnd_level":1018,"humidity":34,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":2.79,"deg":248,"gust":3.04},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-23 12:00:00"},{"dt":1719154800,"main":{"temp":298.25,"feels_like":297.73,"temp_min":298.25,"temp_max":298.25,"pressure":1020,"sea_level":1020,"grnd_level":1017,"humidity":35,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":3},"wind":{"speed":2.91,"deg":272,"gust":2.63},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-23 15:00:00"},{"dt":1719165600,"main":{"temp":297.07,"feels_like":296.64,"temp_min":297.07,"temp_max":297.07,"pressure":1021,"sea_level":1021,"grnd_level":1018,"humidity":43,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":7},"wind":{"speed":2.02,"deg":305,"gust":1.44},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-23 18:00:00"},{"dt":1719176400,"main":{"temp":292.32,"feels_like":292.02,"temp_min":292.32,"temp_max":292.32,"pressure":1022,"sea_level":1022,"grnd_level":1019,"humidity":66,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":8},"wind":{"speed":1.13,"deg":183,"gust":1.99},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-23 21:00:00"},{"dt":1719187200,"main":{"temp":289.93,"feels_like":289.73,"temp_min":289.93,"temp_max":289.93,"pressure":1022,"sea_level":1022,"grnd_level":1018,"humidity":79,"temp_kf":0},"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"clouds":{"all":11},"wind":{"speed":1.31,"deg":215,"gust":2.24},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-24 00:00:00"},{"dt":1719198000,"main":{"temp":288.78,"feels_like":288.64,"temp_min":288.78,"temp_max":288.78,"pressure":1021,"sea_level":1021,"grnd_level":1018,"humidity":86,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":1},"wind":{"speed":0.72,"deg":207,"gust":0.81},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-24 03:00:00"},{"dt":1719208800,"main":{"temp":290.53,"feels_like":290.33,"temp_min":290.53,"temp_max":290.53,"pressure":1021,"sea_level":1021,"grnd_level":1018,"humidity":77,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":1},"wind":{"speed":0.82,"deg":130,"gust":1.08},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-24 06:00:00"},{"dt":1719219600,"main":{"temp":295.46,"feels_like":295.08,"temp_min":295.46,"temp_max":295.46,"pressure":1020,"sea_level":1020,"grnd_level":1017,"humidity":51,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":1.86,"deg":149,"gust":2.24},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-24 09:00:00"},{"dt":1719230400,"main":{"temp":298.92,"feels_like":298.44,"temp_min":298.92,"temp_max":298.92,"pressure":1019,"sea_level":1019,"grnd_level":1016,"humidity":34,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":2.88,"deg":152,"gust":2.67},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-24 12:00:00"},{"dt":1719241200,"main":{"temp":299.59,"feels_like":299.59,"temp_min":299.59,"temp_max":299.59,"pressure":1018,"sea_level":1018,"grnd_level":1015,"humidity":35,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":3.66,"deg":149,"gust":3.05},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-24 15:00:00"},{"dt":1719252000,"main":{"temp":297.25,"feels_like":296.92,"temp_min":297.25,"temp_max":297.25,"pressure":1018,"sea_level":1018,"grnd_level":1015,"humidity":46,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":3.2,"deg":138,"gust":3.52},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2024-06-24 18:00:00"},{"dt":1719262800,"main":{"temp":292.48,"feels_like":292.22,"temp_min":292.48,"temp_max":292.48,"pressure":1018,"sea_level":1018,"grnd_level":1015,"humidity":67,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":0},"wind":{"speed":1.68,"deg":113,"gust":3.06},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-24 21:00:00"},{"dt":1719273600,"main":{"temp":290.9,"feels_like":290.74,"temp_min":290.9,"temp_max":290.9,"pressure":1018,"sea_level":1018,"grnd_level":1015,"humidity":77,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":0},"wind":{"speed":1.29,"deg":117,"gust":2.43},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-25 00:00:00"},{"dt":1719284400,"main":{"temp":289.66,"feels_like":289.4,"temp_min":289.66,"temp_max":289.66,"pressure":1017,"sea_level":1017,"grnd_level":1014,"humidity":78,"temp_kf":0},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"clouds":{"all":10},"wind":{"speed":1.75,"deg":89,"gust":3.28},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-06-25 03:00:00"}],"city":{"id":2643743,"name":"London","coord":{"lat":51.5085,"lon":-0.1257},"country":"GB","population":1000000,"timezone":3600,"sunrise":1718854978,"sunset":1718914877}} 2 | -------------------------------------------------------------------------------- /ExampleMVVMTests/Core/ConfigurationManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationManagerTests.swift 3 | // ExampleMVVMTests 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ExampleMVVM 10 | 11 | class ConfigurationManagerTests: XCTestCase { 12 | func testConfigurationManagerLoadsValues() { 13 | let configurationManager = ConfigurationManager.shared 14 | 15 | XCTAssertNotNil(configurationManager.baseURL) 16 | XCTAssertNotNil(configurationManager.apiKey) 17 | XCTAssertEqual(configurationManager.baseURL, "https://api.openweathermap.org/data/2.5/forecast") 18 | XCTAssertEqual(configurationManager.apiKey, "your_api_key") // replace with your actual key for testing 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ExampleMVVMTests/Data/WeatherRepositoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherRepositoryTests.swift 3 | // ExampleMVVMTests 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ExampleMVVM 10 | 11 | class WeatherRepositoryTests: XCTestCase { 12 | var apiClient: MockAPIClient! 13 | var weatherRepository: WeatherRepositoryImpl! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | apiClient = MockAPIClient() 18 | weatherRepository = WeatherRepositoryImpl(apiClient: apiClient) 19 | } 20 | 21 | override func tearDown() { 22 | apiClient = nil 23 | weatherRepository = nil 24 | super.tearDown() 25 | } 26 | 27 | func testFetchWeatherSuccess() { 28 | let expectation = XCTestExpectation(description: "Fetch weather successfully") 29 | 30 | weatherRepository.fetchWeather(for: "London") { result in 31 | switch result { 32 | case .success(let forecast): 33 | XCTAssertEqual(forecast.city.name, "London") 34 | expectation.fulfill() 35 | case .failure: 36 | XCTFail("Expected success but got failure") 37 | } 38 | } 39 | 40 | wait(for: [expectation], timeout: 5.0) 41 | } 42 | 43 | func testFetchWeatherFailure() { 44 | apiClient.shouldReturnError = true 45 | let expectation = XCTestExpectation(description: "Fetch weather with error") 46 | 47 | weatherRepository.fetchWeather(for: "London") { result in 48 | switch result { 49 | case .success: 50 | XCTFail("Expected failure but got success") 51 | case .failure(let error): 52 | XCTAssertEqual(error.localizedDescription, "Test Error") 53 | expectation.fulfill() 54 | } 55 | } 56 | 57 | wait(for: [expectation], timeout: 5.0) 58 | } 59 | } 60 | 61 | // Mock API Client 62 | class MockAPIClient: APIClient { 63 | var shouldReturnError = false 64 | 65 | override func request(url: URL, method: HTTPMethod, body: Data? = nil, completion: @escaping (Result) -> Void) where T : Decodable { 66 | if shouldReturnError { 67 | completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Test Error"]))) 68 | } else { 69 | let forecast = Forecast(city: City(name: "London", country: "UK"), weatherBundle: []) 70 | completion(.success(forecast as! T)) 71 | } 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /ExampleMVVMTests/Domain/FetchWeatherUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchWeatherUseCaseTests.swift 3 | // ExampleMVVMTests 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ExampleMVVM 10 | 11 | class FetchWeatherUseCaseTests: XCTestCase { 12 | var weatherRepository: MockWeatherRepository! 13 | var fetchWeatherUseCase: FetchWeatherUseCase! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | weatherRepository = MockWeatherRepository() 18 | fetchWeatherUseCase = FetchWeatherUseCase(repository: weatherRepository) 19 | } 20 | 21 | override func tearDown() { 22 | weatherRepository = nil 23 | fetchWeatherUseCase = nil 24 | super.tearDown() 25 | } 26 | 27 | func testExecuteSuccess() { 28 | let expectation = XCTestExpectation(description: "Fetch weather successfully") 29 | 30 | fetchWeatherUseCase.execute(city: "London") { result in 31 | switch result { 32 | case .success(let forecast): 33 | XCTAssertEqual(forecast.city.name, "London") 34 | expectation.fulfill() 35 | case .failure: 36 | XCTFail("Expected success but got failure") 37 | } 38 | } 39 | 40 | wait(for: [expectation], timeout: 5.0) 41 | } 42 | 43 | func testExecuteFailure() { 44 | weatherRepository.shouldReturnError = true 45 | let expectation = XCTestExpectation(description: "Fetch weather with error") 46 | 47 | fetchWeatherUseCase.execute(city: "London") { result in 48 | switch result { 49 | case .success: 50 | XCTFail("Expected failure but got success") 51 | case .failure(let error): 52 | XCTAssertEqual(error.localizedDescription, "Test Error") 53 | expectation.fulfill() 54 | } 55 | } 56 | 57 | wait(for: [expectation], timeout: 5.0) 58 | } 59 | } 60 | 61 | // Mock Weather Repository 62 | class MockWeatherRepository: WeatherRepository { 63 | var shouldReturnError = false 64 | 65 | func fetchWeather(for city: String, completion: @escaping (Result) -> Void) { 66 | if shouldReturnError { 67 | completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Test Error"]))) 68 | } else { 69 | let forecast = Forecast(city: City(name: "London", country: "UK"), weatherBundle: []) 70 | completion(.success(forecast)) 71 | } 72 | } 73 | 74 | func updateWeather(for city: String, with data: Data, completion: @escaping (Result) -> Void) { 75 | // Not needed for this test 76 | } 77 | 78 | func deleteWeather(for city: String, completion: @escaping (Result) -> Void) { 79 | // Not needed for this test 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /ExampleMVVMTests/ExampleMVVMTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleMVVMTests.swift 3 | // ExampleMVVMTests 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ExampleMVVM 10 | 11 | final class ExampleMVVMTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ExampleMVVMTests/Infrastructure/APIClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClientTests.swift 3 | // ExampleMVVMTests 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ExampleMVVM 10 | 11 | class APIClientTests: XCTestCase { 12 | var apiClient: APIClient! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | apiClient = APIClient(session: URLSessionMock()) 17 | } 18 | 19 | override func tearDown() { 20 | apiClient = nil 21 | super.tearDown() 22 | } 23 | 24 | func testRequestSuccess() { 25 | // Add your test cases here 26 | } 27 | 28 | func testRequestFailure() { 29 | // Add your test cases here 30 | } 31 | } 32 | 33 | // Mock URLSession 34 | class URLSessionMock: URLSession { 35 | var cachedUrl: URL? 36 | var completionHandler: ((Data?, URLResponse?, Error?) -> Void)? 37 | 38 | override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 39 | self.cachedUrl = url 40 | self.completionHandler = completionHandler 41 | return URLSessionDataTaskMock() 42 | } 43 | } 44 | 45 | class URLSessionDataTaskMock: URLSessionDataTask { 46 | override func resume() { 47 | // do nothing 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /ExampleMVVMTests/Presentation/WeatherViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherViewModelTests.swift 3 | // ExampleMVVMTests 4 | // 5 | // Created by MacBook Air M1 on 20/6/24. 6 | // 7 | 8 | import XCTest 9 | @testable import ExampleMVVM 10 | 11 | class WeatherViewModelTests: XCTestCase { 12 | var weatherRepository: MockWeatherRepository! 13 | var fetchWeatherUseCase: FetchWeatherUseCase! 14 | var weatherViewModel: WeatherViewModel! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | weatherRepository = MockWeatherRepository() 19 | fetchWeatherUseCase = FetchWeatherUseCase(repository: weatherRepository) 20 | weatherViewModel = WeatherViewModel(fetchWeatherUseCase: fetchWeatherUseCase) 21 | } 22 | 23 | override func tearDown() { 24 | weatherRepository = nil 25 | fetchWeatherUseCase = nil 26 | weatherViewModel = nil 27 | super.tearDown() 28 | } 29 | 30 | func testFetchWeatherSuccess() { 31 | let expectation = XCTestExpectation(description: "Fetch weather successfully") 32 | 33 | weatherViewModel.fetchWeather(for: "London") 34 | 35 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 36 | XCTAssertNotNil(self.weatherViewModel.forecast) 37 | XCTAssertEqual(self.weatherViewModel.forecast?.city.name, "London") 38 | expectation.fulfill() 39 | } 40 | 41 | wait(for: [expectation], timeout: 5.0) 42 | } 43 | 44 | func testFetchWeatherFailure() { 45 | weatherRepository.shouldReturnError = true 46 | let expectation = XCTestExpectation(description: "Fetch weather with error") 47 | 48 | weatherViewModel.fetchWeather(for: "London") 49 | 50 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 51 | XCTAssertNotNil(self.weatherViewModel.errorMessage) 52 | XCTAssertEqual(self.weatherViewModel.errorMessage, "Test Error") 53 | expectation.fulfill() 54 | } 55 | 56 | wait(for: [expectation], timeout: 5.0) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /ExampleMVVMUITests/ExampleMVVMUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleMVVMUITests.swift 3 | // ExampleMVVMUITests 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ExampleMVVMUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // 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. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ExampleMVVMUITests/ExampleMVVMUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleMVVMUITestsLaunchTests.swift 3 | // ExampleMVVMUITests 4 | // 5 | // Created by MacBook Air M1 on 19/6/24. 6 | // 7 | 8 | import XCTest 9 | 10 | final class ExampleMVVMUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture and SwiftUI Modular Project 2 | 3 | ## Overview 4 | 5 | ExampleMVVM is an iOS application designed with Clean Architecture principles and SwiftUI modularity. This project demonstrates a scalable, maintainable, and testable architecture, leveraging dependency injection, protocol-oriented programming, and clear separation of concerns. 6 | 7 | ## Project Structure 8 | 9 | The project is organized into distinct layers, each responsible for specific concerns: 10 | 11 | - **App**: Entry point and application lifecycle management. 12 | - **Presentation**: UI components and view models. 13 | - **Domain**: Business logic and use cases. 14 | - **Data**: Data sources and repositories. 15 | - **Infrastructure**: Configuration, network, and utilities. 16 | 17 | ### Directory Layout 18 | Screenshot 2024-06-20 at 9 59 02 AM 19 | 20 | 21 | ## Layer Responsibilities 22 | 23 | ### App 24 | 25 | **ExampleMVVMApp.swift**: Entry point of the application, responsible for setting up the main window and initializing the dependency injection. 26 | 27 | ### Presentation 28 | 29 | **View**: Contains SwiftUI views responsible for the UI layout and presentation. 30 | 31 | - **WeatherView.swift**: Main view displaying weather information. 32 | - **WeatherRowView.swift**: Subview displaying individual weather data items. 33 | 34 | **ViewModel**: Contains view models that manage UI state and interact with the domain layer. 35 | 36 | - **WeatherViewModel.swift**: View model for managing weather data presentation logic. 37 | 38 | ### Domain 39 | 40 | **Interfaces**: Defines protocols for repositories and data sources, ensuring a clear contract between layers. 41 | 42 | - **WeatherDataSource.swift**: Protocol for weather data sources. 43 | - **WeatherRepository.swift**: Protocol for weather repositories. 44 | 45 | **UseCases**: Contains business logic and application-specific rules. 46 | 47 | - **FetchWeatherUseCase.swift**: Use case for fetching weather data. 48 | 49 | **Entities**: Contains business models representing core domain objects. 50 | 51 | - **Weather.swift**, **City.swift**, **Temperature.swift**: Domain models for weather data. 52 | 53 | ### Data 54 | 55 | **Remote**: Contains data sources for fetching data from remote APIs. 56 | 57 | - **RemoteWeatherDataSource.swift**: Implementation of weather data source using remote APIs. 58 | 59 | **Local**: Contains data sources for fetching data from local files. 60 | 61 | - **LocalWeatherDataSource.swift**: Implementation of weather data source using local JSON files. 62 | 63 | **Repositories**: Coordinates between data sources and domain layer, transforming data as needed. 64 | 65 | - **WeatherRepositoryImpl.swift**: Implementation of weather repository. 66 | 67 | ### Infrastructure 68 | 69 | **Network**: Manages network communication details. 70 | 71 | - **APIClient.swift**: Handles HTTP requests and responses. 72 | - **HTTPMethod.swift**: Enum for HTTP methods. 73 | - **WeatherAPIConfiguration.swift**: Constructs URLs for API endpoints. 74 | 75 | **Configuration**: Manages application configurations. 76 | 77 | - **AppConfiguration.swift**: Implementation of configuration protocol. 78 | 79 | **Utilities**: Contains shared utilities and helpers. 80 | 81 | - **JSONLoader.swift**: Utility for loading JSON from local files. 82 | 83 | **Interfaces**: Defines protocols for infrastructure components. 84 | 85 | - **JSONLoaderProtocol.swift**: Protocol for JSON loader. 86 | - **APIClientProtocol.swift**: Protocol for API client. 87 | - **ConfigurationProtocol.swift**: Protocol for configuration. 88 | 89 | ## Dependency Injection 90 | 91 | The project leverages dependency injection to manage dependencies between layers. This ensures loose coupling and enhances testability. Dependencies are injected through initializers, allowing for easy substitution of implementations during testing. 92 | 93 | ### Example Initialization 94 | 95 | ```swift 96 | import SwiftUI 97 | 98 | @main 99 | struct ExampleMVVMApp: App { 100 | var body: some Scene { 101 | WindowGroup { 102 | let apiClient = APIClient() 103 | let appConfiguration = AppConfiguration() 104 | let apiConfiguration = WeatherAPIConfiguration(configuration: appConfiguration) 105 | let remoteDataSource = RemoteWeatherDataSource(apiClient: apiClient, apiConfiguration: apiConfiguration) 106 | 107 | let jsonLoader = JSONLoader() 108 | let localDataSource = LocalWeatherDataSource(jsonLoader: jsonLoader, configuration: appConfiguration) 109 | 110 | let useLocalData = false // Change this to true to use local data 111 | let dataSource: WeatherDataSource = useLocalData ? localDataSource : remoteDataSource 112 | 113 | let repository = WeatherRepositoryImpl(dataSource: dataSource) 114 | let fetchWeatherUseCase = FetchWeatherUseCase(repository: repository) 115 | let viewModel = WeatherViewModel(fetchWeatherUseCase: fetchWeatherUseCase) 116 | 117 | WeatherView(viewModel: viewModel) 118 | } 119 | } 120 | } 121 | ``` 122 | --------------------------------------------------------------------------------