├── .gitignore ├── ModularSwiftUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── ModularSwiftUI.xcscheme ├── ModularSwiftUI ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Common │ ├── Coordinator │ │ ├── CharacterCoordinator.swift │ │ └── CharacterCoordinatorView.swift │ ├── DI │ │ ├── AppContainer.swift │ │ ├── CharacterDetailsContainer.swift │ │ └── CharactersListContainer.swift │ ├── Networking │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources │ │ │ └── Networking │ │ │ │ ├── Networking.swift │ │ │ │ └── URLSessionClient.swift │ │ └── Tests │ │ │ └── NetworkingTests │ │ │ └── NetworkingTests.swift │ ├── NetworkingInterface │ │ ├── .gitignore │ │ ├── .swiftpm │ │ │ └── xcode │ │ │ │ └── package.xcworkspace │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources │ │ │ └── NetworkingInterface │ │ │ │ ├── HTTPClient.swift │ │ │ │ ├── NetworkingRequest.swift │ │ │ │ └── NetworkingResponse.swift │ │ └── Tests │ │ │ └── NetworkingInterfaceTests │ │ │ └── NetworkingInterfaceTests.swift │ └── Utilities │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources │ │ └── Utilities │ │ │ ├── View+Navigation.swift │ │ │ ├── View+errorAlert.swift │ │ │ ├── View+isHidden.swift │ │ │ └── View+onViewDidLoad.swift │ │ └── Tests │ │ └── UtilitiesTests │ │ └── UtilitiesTests.swift ├── ModularSwiftUIApp.swift ├── Modules │ ├── CharacterDetails │ │ ├── .gitignore │ │ ├── .swiftpm │ │ │ └── xcode │ │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── CharacterDetails.xcscheme │ │ ├── Package.resolved │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources │ │ │ └── CharacterDetails │ │ │ │ ├── View │ │ │ │ ├── CharacterDetails.swift │ │ │ │ └── CircleImage.swift │ │ │ │ └── ViewModel │ │ │ │ └── CharacterDetailsViewModel.swift │ │ └── Tests │ │ │ └── CharacterDetailsTests │ │ │ └── CharacterDetailsTests.swift │ ├── CharacterList │ │ ├── .gitignore │ │ ├── .swiftpm │ │ │ └── xcode │ │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── CharacterList.xcscheme │ │ ├── Package.resolved │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources │ │ │ └── CharacterList │ │ │ │ ├── Networking │ │ │ │ ├── CharacterListNetworkService.swift │ │ │ │ ├── CharactersRequest.swift │ │ │ │ └── CharactersResponse.swift │ │ │ │ ├── UseCase │ │ │ │ └── CharacterListUseCase.swift │ │ │ │ ├── View │ │ │ │ ├── Caption.swift │ │ │ │ ├── CharacterList.swift │ │ │ │ └── CharacterRow.swift │ │ │ │ └── ViewModel │ │ │ │ └── CharacterListViewModel.swift │ │ └── Tests │ │ │ └── CharacterListTests │ │ │ ├── CharacterListViewModelDoubles.swift │ │ │ └── CharacterListViewModelTests.swift │ └── CharacterModels │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources │ │ └── CharacterModels │ │ │ └── CharacterModels.swift │ │ └── Tests │ │ └── CharacterModelsTests │ │ └── CharacterModelsTests.swift └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | -------------------------------------------------------------------------------- /ModularSwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A346D45F28E72E1C004AEF78 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = A346D45E28E72E1C004AEF78 /* Networking */; }; 11 | A389C47128E7536D00567CFE /* CharacterDetailsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A389C47028E7536D00567CFE /* CharacterDetailsContainer.swift */; }; 12 | A389C47428E7542200567CFE /* Swinject in Frameworks */ = {isa = PBXBuildFile; productRef = A389C47328E7542200567CFE /* Swinject */; }; 13 | A395D71528E5B445002F719C /* CharacterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A395D71428E5B445002F719C /* CharacterCoordinator.swift */; }; 14 | A395D71728E5B5F9002F719C /* CharacterCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A395D71628E5B5F9002F719C /* CharacterCoordinatorView.swift */; }; 15 | A395D71D28E5C39C002F719C /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A395D71C28E5C39C002F719C /* AppContainer.swift */; }; 16 | A395D71F28E5CDA4002F719C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A395D71E28E5CDA4002F719C /* AppDelegate.swift */; }; 17 | A3AF32CB28E72E9200E6445E /* NetworkingInterface in Frameworks */ = {isa = PBXBuildFile; productRef = A3AF32CA28E72E9200E6445E /* NetworkingInterface */; }; 18 | A3CA7F7028E6EE95008DF69B /* CharactersListContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3CA7F6F28E6EE95008DF69B /* CharactersListContainer.swift */; }; 19 | A3EC370B28E1AF4900329FF4 /* ModularSwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3EC370A28E1AF4900329FF4 /* ModularSwiftUIApp.swift */; }; 20 | A3EC370F28E1AF4A00329FF4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A3EC370E28E1AF4A00329FF4 /* Assets.xcassets */; }; 21 | A3EC371228E1AF4A00329FF4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A3EC371128E1AF4A00329FF4 /* Preview Assets.xcassets */; }; 22 | A3EE3D8428E70D97004AD9B7 /* CharacterDetails in Frameworks */ = {isa = PBXBuildFile; productRef = A3EE3D8328E70D97004AD9B7 /* CharacterDetails */; }; 23 | A3EE3D8628E70D9A004AD9B7 /* CharacterList in Frameworks */ = {isa = PBXBuildFile; productRef = A3EE3D8528E70D9A004AD9B7 /* CharacterList */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | A373451C28E23EC500DA7143 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = A3EC36FF28E1AF4900329FF4 /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = A3EC370628E1AF4900329FF4; 32 | remoteInfo = ModularSwiftUI; 33 | }; 34 | /* End PBXContainerItemProxy section */ 35 | 36 | /* Begin PBXFileReference section */ 37 | A324610328E732570073E7D8 /* CharacterModels */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CharacterModels; sourceTree = ""; }; 38 | A3413BB828E7244D003F1CC1 /* Utilities */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Utilities; path = ModularSwiftUI/Common/Utilities; sourceTree = ""; }; 39 | A373451828E23EC500DA7143 /* ModularSwiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ModularSwiftUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | A389C47028E7536D00567CFE /* CharacterDetailsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterDetailsContainer.swift; sourceTree = ""; }; 41 | A395D71428E5B445002F719C /* CharacterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCoordinator.swift; sourceTree = ""; }; 42 | A395D71628E5B5F9002F719C /* CharacterCoordinatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCoordinatorView.swift; sourceTree = ""; }; 43 | A395D71C28E5C39C002F719C /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContainer.swift; sourceTree = ""; }; 44 | A395D71E28E5CDA4002F719C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 45 | A3AB78E328E72CDE00FD3C44 /* Networking */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Networking; path = ModularSwiftUI/Common/Networking; sourceTree = ""; }; 46 | A3CA7F6F28E6EE95008DF69B /* CharactersListContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharactersListContainer.swift; sourceTree = ""; }; 47 | A3EC370728E1AF4900329FF4 /* ModularSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ModularSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | A3EC370A28E1AF4900329FF4 /* ModularSwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModularSwiftUIApp.swift; sourceTree = ""; }; 49 | A3EC370E28E1AF4A00329FF4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50 | A3EC371128E1AF4A00329FF4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 51 | A3EE3D8028E70CFF004AD9B7 /* NetworkingInterface */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NetworkingInterface; path = ModularSwiftUI/Common/NetworkingInterface; sourceTree = ""; }; 52 | A3EE3D8128E70D0C004AD9B7 /* CharacterList */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CharacterList; sourceTree = ""; }; 53 | A3EE3D8228E70D1D004AD9B7 /* CharacterDetails */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CharacterDetails; sourceTree = ""; }; 54 | /* End PBXFileReference section */ 55 | 56 | /* Begin PBXFrameworksBuildPhase section */ 57 | A373451528E23EC500DA7143 /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | A3EC370428E1AF4900329FF4 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | A3AF32CB28E72E9200E6445E /* NetworkingInterface in Frameworks */, 69 | A389C47428E7542200567CFE /* Swinject in Frameworks */, 70 | A346D45F28E72E1C004AEF78 /* Networking in Frameworks */, 71 | A3EE3D8628E70D9A004AD9B7 /* CharacterList in Frameworks */, 72 | A3EE3D8428E70D97004AD9B7 /* CharacterDetails in Frameworks */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | A37344F528E218DB00DA7143 /* Modules */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | A324610328E732570073E7D8 /* CharacterModels */, 83 | A3EE3D8128E70D0C004AD9B7 /* CharacterList */, 84 | A3EE3D8228E70D1D004AD9B7 /* CharacterDetails */, 85 | ); 86 | path = Modules; 87 | sourceTree = ""; 88 | }; 89 | A37344F828E219A900DA7143 /* Common */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | A395D71B28E5C384002F719C /* DI */, 93 | A395D71A28E5C0C2002F719C /* Coordinator */, 94 | ); 95 | path = Common; 96 | sourceTree = ""; 97 | }; 98 | A395D71A28E5C0C2002F719C /* Coordinator */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | A395D71428E5B445002F719C /* CharacterCoordinator.swift */, 102 | A395D71628E5B5F9002F719C /* CharacterCoordinatorView.swift */, 103 | ); 104 | path = Coordinator; 105 | sourceTree = ""; 106 | }; 107 | A395D71B28E5C384002F719C /* DI */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | A395D71C28E5C39C002F719C /* AppContainer.swift */, 111 | A3CA7F6F28E6EE95008DF69B /* CharactersListContainer.swift */, 112 | A389C47028E7536D00567CFE /* CharacterDetailsContainer.swift */, 113 | ); 114 | path = DI; 115 | sourceTree = ""; 116 | }; 117 | A3CA7F6C28E6EB94008DF69B /* Frameworks */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | ); 121 | name = Frameworks; 122 | sourceTree = ""; 123 | }; 124 | A3EC36FE28E1AF4900329FF4 = { 125 | isa = PBXGroup; 126 | children = ( 127 | A3EE3D7F28E70CFF004AD9B7 /* Packages */, 128 | A3EC370928E1AF4900329FF4 /* ModularSwiftUI */, 129 | A3EC370828E1AF4900329FF4 /* Products */, 130 | A3CA7F6C28E6EB94008DF69B /* Frameworks */, 131 | ); 132 | sourceTree = ""; 133 | }; 134 | A3EC370828E1AF4900329FF4 /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | A3EC370728E1AF4900329FF4 /* ModularSwiftUI.app */, 138 | A373451828E23EC500DA7143 /* ModularSwiftUITests.xctest */, 139 | ); 140 | name = Products; 141 | sourceTree = ""; 142 | }; 143 | A3EC370928E1AF4900329FF4 /* ModularSwiftUI */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | A3EC370A28E1AF4900329FF4 /* ModularSwiftUIApp.swift */, 147 | A395D71E28E5CDA4002F719C /* AppDelegate.swift */, 148 | A37344F828E219A900DA7143 /* Common */, 149 | A37344F528E218DB00DA7143 /* Modules */, 150 | A3EC370E28E1AF4A00329FF4 /* Assets.xcassets */, 151 | A3EC371028E1AF4A00329FF4 /* Preview Content */, 152 | ); 153 | path = ModularSwiftUI; 154 | sourceTree = ""; 155 | }; 156 | A3EC371028E1AF4A00329FF4 /* Preview Content */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | A3EC371128E1AF4A00329FF4 /* Preview Assets.xcassets */, 160 | ); 161 | path = "Preview Content"; 162 | sourceTree = ""; 163 | }; 164 | A3EE3D7F28E70CFF004AD9B7 /* Packages */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | A3EE3D8028E70CFF004AD9B7 /* NetworkingInterface */, 168 | A3AB78E328E72CDE00FD3C44 /* Networking */, 169 | A3413BB828E7244D003F1CC1 /* Utilities */, 170 | ); 171 | name = Packages; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXGroup section */ 175 | 176 | /* Begin PBXNativeTarget section */ 177 | A373451728E23EC500DA7143 /* ModularSwiftUITests */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = A373451E28E23EC500DA7143 /* Build configuration list for PBXNativeTarget "ModularSwiftUITests" */; 180 | buildPhases = ( 181 | A373451428E23EC500DA7143 /* Sources */, 182 | A373451528E23EC500DA7143 /* Frameworks */, 183 | A373451628E23EC500DA7143 /* Resources */, 184 | ); 185 | buildRules = ( 186 | ); 187 | dependencies = ( 188 | A373451D28E23EC500DA7143 /* PBXTargetDependency */, 189 | ); 190 | name = ModularSwiftUITests; 191 | productName = ModularSwiftUITests; 192 | productReference = A373451828E23EC500DA7143 /* ModularSwiftUITests.xctest */; 193 | productType = "com.apple.product-type.bundle.unit-test"; 194 | }; 195 | A3EC370628E1AF4900329FF4 /* ModularSwiftUI */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = A3EC371528E1AF4A00329FF4 /* Build configuration list for PBXNativeTarget "ModularSwiftUI" */; 198 | buildPhases = ( 199 | A3EC370328E1AF4900329FF4 /* Sources */, 200 | A3EC370428E1AF4900329FF4 /* Frameworks */, 201 | A3EC370528E1AF4900329FF4 /* Resources */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | ); 207 | name = ModularSwiftUI; 208 | packageProductDependencies = ( 209 | A3EE3D8328E70D97004AD9B7 /* CharacterDetails */, 210 | A3EE3D8528E70D9A004AD9B7 /* CharacterList */, 211 | A346D45E28E72E1C004AEF78 /* Networking */, 212 | A3AF32CA28E72E9200E6445E /* NetworkingInterface */, 213 | A389C47328E7542200567CFE /* Swinject */, 214 | ); 215 | productName = ModularSwiftUI; 216 | productReference = A3EC370728E1AF4900329FF4 /* ModularSwiftUI.app */; 217 | productType = "com.apple.product-type.application"; 218 | }; 219 | /* End PBXNativeTarget section */ 220 | 221 | /* Begin PBXProject section */ 222 | A3EC36FF28E1AF4900329FF4 /* Project object */ = { 223 | isa = PBXProject; 224 | attributes = { 225 | BuildIndependentTargetsInParallel = 1; 226 | LastSwiftUpdateCheck = 1310; 227 | LastUpgradeCheck = 1310; 228 | TargetAttributes = { 229 | A373451728E23EC500DA7143 = { 230 | CreatedOnToolsVersion = 13.1; 231 | TestTargetID = A3EC370628E1AF4900329FF4; 232 | }; 233 | A3EC370628E1AF4900329FF4 = { 234 | CreatedOnToolsVersion = 13.1; 235 | }; 236 | }; 237 | }; 238 | buildConfigurationList = A3EC370228E1AF4900329FF4 /* Build configuration list for PBXProject "ModularSwiftUI" */; 239 | compatibilityVersion = "Xcode 13.0"; 240 | developmentRegion = en; 241 | hasScannedForEncodings = 0; 242 | knownRegions = ( 243 | en, 244 | Base, 245 | ); 246 | mainGroup = A3EC36FE28E1AF4900329FF4; 247 | packageReferences = ( 248 | A389C47228E7542200567CFE /* XCRemoteSwiftPackageReference "Swinject" */, 249 | ); 250 | productRefGroup = A3EC370828E1AF4900329FF4 /* Products */; 251 | projectDirPath = ""; 252 | projectRoot = ""; 253 | targets = ( 254 | A3EC370628E1AF4900329FF4 /* ModularSwiftUI */, 255 | A373451728E23EC500DA7143 /* ModularSwiftUITests */, 256 | ); 257 | }; 258 | /* End PBXProject section */ 259 | 260 | /* Begin PBXResourcesBuildPhase section */ 261 | A373451628E23EC500DA7143 /* Resources */ = { 262 | isa = PBXResourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | A3EC370528E1AF4900329FF4 /* Resources */ = { 269 | isa = PBXResourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | A3EC371228E1AF4A00329FF4 /* Preview Assets.xcassets in Resources */, 273 | A3EC370F28E1AF4A00329FF4 /* Assets.xcassets in Resources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXResourcesBuildPhase section */ 278 | 279 | /* Begin PBXSourcesBuildPhase section */ 280 | A373451428E23EC500DA7143 /* Sources */ = { 281 | isa = PBXSourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | A3EC370328E1AF4900329FF4 /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | A395D71F28E5CDA4002F719C /* AppDelegate.swift in Sources */, 292 | A395D71528E5B445002F719C /* CharacterCoordinator.swift in Sources */, 293 | A395D71728E5B5F9002F719C /* CharacterCoordinatorView.swift in Sources */, 294 | A3EC370B28E1AF4900329FF4 /* ModularSwiftUIApp.swift in Sources */, 295 | A389C47128E7536D00567CFE /* CharacterDetailsContainer.swift in Sources */, 296 | A3CA7F7028E6EE95008DF69B /* CharactersListContainer.swift in Sources */, 297 | A395D71D28E5C39C002F719C /* AppContainer.swift in Sources */, 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | /* End PBXSourcesBuildPhase section */ 302 | 303 | /* Begin PBXTargetDependency section */ 304 | A373451D28E23EC500DA7143 /* PBXTargetDependency */ = { 305 | isa = PBXTargetDependency; 306 | target = A3EC370628E1AF4900329FF4 /* ModularSwiftUI */; 307 | targetProxy = A373451C28E23EC500DA7143 /* PBXContainerItemProxy */; 308 | }; 309 | /* End PBXTargetDependency section */ 310 | 311 | /* Begin XCBuildConfiguration section */ 312 | A373451F28E23EC500DA7143 /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | BUNDLE_LOADER = "$(TEST_HOST)"; 316 | CODE_SIGN_STYLE = Automatic; 317 | CURRENT_PROJECT_VERSION = 1; 318 | DEVELOPMENT_TEAM = T7928YUM2A; 319 | GENERATE_INFOPLIST_FILE = YES; 320 | LD_RUNPATH_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "@executable_path/Frameworks", 323 | "@loader_path/Frameworks", 324 | ); 325 | MARKETING_VERSION = 1.0; 326 | PRODUCT_BUNDLE_IDENTIFIER = nafie.ModularSwiftUITests; 327 | PRODUCT_NAME = "$(TARGET_NAME)"; 328 | SWIFT_EMIT_LOC_STRINGS = NO; 329 | SWIFT_VERSION = 5.0; 330 | TARGETED_DEVICE_FAMILY = "1,2"; 331 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ModularSwiftUI.app/ModularSwiftUI"; 332 | }; 333 | name = Debug; 334 | }; 335 | A373452028E23EC500DA7143 /* Release */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | BUNDLE_LOADER = "$(TEST_HOST)"; 339 | CODE_SIGN_STYLE = Automatic; 340 | CURRENT_PROJECT_VERSION = 1; 341 | DEVELOPMENT_TEAM = T7928YUM2A; 342 | GENERATE_INFOPLIST_FILE = YES; 343 | LD_RUNPATH_SEARCH_PATHS = ( 344 | "$(inherited)", 345 | "@executable_path/Frameworks", 346 | "@loader_path/Frameworks", 347 | ); 348 | MARKETING_VERSION = 1.0; 349 | PRODUCT_BUNDLE_IDENTIFIER = nafie.ModularSwiftUITests; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | SWIFT_EMIT_LOC_STRINGS = NO; 352 | SWIFT_VERSION = 5.0; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ModularSwiftUI.app/ModularSwiftUI"; 355 | }; 356 | name = Release; 357 | }; 358 | A3EC371328E1AF4A00329FF4 /* Debug */ = { 359 | isa = XCBuildConfiguration; 360 | buildSettings = { 361 | ALWAYS_SEARCH_USER_PATHS = NO; 362 | CLANG_ANALYZER_NONNULL = YES; 363 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 364 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 365 | CLANG_CXX_LIBRARY = "libc++"; 366 | CLANG_ENABLE_MODULES = YES; 367 | CLANG_ENABLE_OBJC_ARC = YES; 368 | CLANG_ENABLE_OBJC_WEAK = YES; 369 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 370 | CLANG_WARN_BOOL_CONVERSION = YES; 371 | CLANG_WARN_COMMA = YES; 372 | CLANG_WARN_CONSTANT_CONVERSION = YES; 373 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 374 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 375 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 376 | CLANG_WARN_EMPTY_BODY = YES; 377 | CLANG_WARN_ENUM_CONVERSION = YES; 378 | CLANG_WARN_INFINITE_RECURSION = YES; 379 | CLANG_WARN_INT_CONVERSION = YES; 380 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 381 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 382 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 383 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 384 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 385 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 386 | CLANG_WARN_STRICT_PROTOTYPES = YES; 387 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 388 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 389 | CLANG_WARN_UNREACHABLE_CODE = YES; 390 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 391 | COPY_PHASE_STRIP = NO; 392 | DEBUG_INFORMATION_FORMAT = dwarf; 393 | ENABLE_STRICT_OBJC_MSGSEND = YES; 394 | ENABLE_TESTABILITY = YES; 395 | GCC_C_LANGUAGE_STANDARD = gnu11; 396 | GCC_DYNAMIC_NO_PIC = NO; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_OPTIMIZATION_LEVEL = 0; 399 | GCC_PREPROCESSOR_DEFINITIONS = ( 400 | "DEBUG=1", 401 | "$(inherited)", 402 | ); 403 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 404 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 405 | GCC_WARN_UNDECLARED_SELECTOR = YES; 406 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 407 | GCC_WARN_UNUSED_FUNCTION = YES; 408 | GCC_WARN_UNUSED_VARIABLE = YES; 409 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 410 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 411 | MTL_FAST_MATH = YES; 412 | ONLY_ACTIVE_ARCH = YES; 413 | SDKROOT = iphoneos; 414 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 415 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 416 | }; 417 | name = Debug; 418 | }; 419 | A3EC371428E1AF4A00329FF4 /* Release */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ALWAYS_SEARCH_USER_PATHS = NO; 423 | CLANG_ANALYZER_NONNULL = YES; 424 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 425 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 426 | CLANG_CXX_LIBRARY = "libc++"; 427 | CLANG_ENABLE_MODULES = YES; 428 | CLANG_ENABLE_OBJC_ARC = YES; 429 | CLANG_ENABLE_OBJC_WEAK = YES; 430 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 431 | CLANG_WARN_BOOL_CONVERSION = YES; 432 | CLANG_WARN_COMMA = YES; 433 | CLANG_WARN_CONSTANT_CONVERSION = YES; 434 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 435 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 436 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 437 | CLANG_WARN_EMPTY_BODY = YES; 438 | CLANG_WARN_ENUM_CONVERSION = YES; 439 | CLANG_WARN_INFINITE_RECURSION = YES; 440 | CLANG_WARN_INT_CONVERSION = YES; 441 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 442 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 443 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 444 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 445 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 446 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 447 | CLANG_WARN_STRICT_PROTOTYPES = YES; 448 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 449 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 450 | CLANG_WARN_UNREACHABLE_CODE = YES; 451 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 452 | COPY_PHASE_STRIP = NO; 453 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 454 | ENABLE_NS_ASSERTIONS = NO; 455 | ENABLE_STRICT_OBJC_MSGSEND = YES; 456 | GCC_C_LANGUAGE_STANDARD = gnu11; 457 | GCC_NO_COMMON_BLOCKS = YES; 458 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 459 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 460 | GCC_WARN_UNDECLARED_SELECTOR = YES; 461 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 462 | GCC_WARN_UNUSED_FUNCTION = YES; 463 | GCC_WARN_UNUSED_VARIABLE = YES; 464 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 465 | MTL_ENABLE_DEBUG_INFO = NO; 466 | MTL_FAST_MATH = YES; 467 | SDKROOT = iphoneos; 468 | SWIFT_COMPILATION_MODE = wholemodule; 469 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 470 | VALIDATE_PRODUCT = YES; 471 | }; 472 | name = Release; 473 | }; 474 | A3EC371628E1AF4A00329FF4 /* Debug */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 479 | CODE_SIGN_STYLE = Automatic; 480 | CURRENT_PROJECT_VERSION = 1; 481 | DEVELOPMENT_ASSET_PATHS = "\"ModularSwiftUI/Preview Content\""; 482 | DEVELOPMENT_TEAM = T7928YUM2A; 483 | ENABLE_PREVIEWS = YES; 484 | GENERATE_INFOPLIST_FILE = YES; 485 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 486 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 487 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 488 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 489 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 490 | LD_RUNPATH_SEARCH_PATHS = ( 491 | "$(inherited)", 492 | "@executable_path/Frameworks", 493 | ); 494 | MARKETING_VERSION = 1.0; 495 | PRODUCT_BUNDLE_IDENTIFIER = nafie.ModularSwiftUI; 496 | PRODUCT_NAME = "$(TARGET_NAME)"; 497 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 498 | SUPPORTS_MACCATALYST = NO; 499 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 500 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 501 | SWIFT_EMIT_LOC_STRINGS = YES; 502 | SWIFT_VERSION = 5.0; 503 | TARGETED_DEVICE_FAMILY = 1; 504 | }; 505 | name = Debug; 506 | }; 507 | A3EC371728E1AF4A00329FF4 /* Release */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 511 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 512 | CODE_SIGN_STYLE = Automatic; 513 | CURRENT_PROJECT_VERSION = 1; 514 | DEVELOPMENT_ASSET_PATHS = "\"ModularSwiftUI/Preview Content\""; 515 | DEVELOPMENT_TEAM = T7928YUM2A; 516 | ENABLE_PREVIEWS = YES; 517 | GENERATE_INFOPLIST_FILE = YES; 518 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 519 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 520 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 521 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 522 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 523 | LD_RUNPATH_SEARCH_PATHS = ( 524 | "$(inherited)", 525 | "@executable_path/Frameworks", 526 | ); 527 | MARKETING_VERSION = 1.0; 528 | PRODUCT_BUNDLE_IDENTIFIER = nafie.ModularSwiftUI; 529 | PRODUCT_NAME = "$(TARGET_NAME)"; 530 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 531 | SUPPORTS_MACCATALYST = NO; 532 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 533 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 534 | SWIFT_EMIT_LOC_STRINGS = YES; 535 | SWIFT_VERSION = 5.0; 536 | TARGETED_DEVICE_FAMILY = 1; 537 | }; 538 | name = Release; 539 | }; 540 | /* End XCBuildConfiguration section */ 541 | 542 | /* Begin XCConfigurationList section */ 543 | A373451E28E23EC500DA7143 /* Build configuration list for PBXNativeTarget "ModularSwiftUITests" */ = { 544 | isa = XCConfigurationList; 545 | buildConfigurations = ( 546 | A373451F28E23EC500DA7143 /* Debug */, 547 | A373452028E23EC500DA7143 /* Release */, 548 | ); 549 | defaultConfigurationIsVisible = 0; 550 | defaultConfigurationName = Release; 551 | }; 552 | A3EC370228E1AF4900329FF4 /* Build configuration list for PBXProject "ModularSwiftUI" */ = { 553 | isa = XCConfigurationList; 554 | buildConfigurations = ( 555 | A3EC371328E1AF4A00329FF4 /* Debug */, 556 | A3EC371428E1AF4A00329FF4 /* Release */, 557 | ); 558 | defaultConfigurationIsVisible = 0; 559 | defaultConfigurationName = Release; 560 | }; 561 | A3EC371528E1AF4A00329FF4 /* Build configuration list for PBXNativeTarget "ModularSwiftUI" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | A3EC371628E1AF4A00329FF4 /* Debug */, 565 | A3EC371728E1AF4A00329FF4 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | /* End XCConfigurationList section */ 571 | 572 | /* Begin XCRemoteSwiftPackageReference section */ 573 | A389C47228E7542200567CFE /* XCRemoteSwiftPackageReference "Swinject" */ = { 574 | isa = XCRemoteSwiftPackageReference; 575 | repositoryURL = "https://github.com/Swinject/Swinject.git"; 576 | requirement = { 577 | kind = upToNextMajorVersion; 578 | minimumVersion = 2.0.0; 579 | }; 580 | }; 581 | /* End XCRemoteSwiftPackageReference section */ 582 | 583 | /* Begin XCSwiftPackageProductDependency section */ 584 | A346D45E28E72E1C004AEF78 /* Networking */ = { 585 | isa = XCSwiftPackageProductDependency; 586 | productName = Networking; 587 | }; 588 | A389C47328E7542200567CFE /* Swinject */ = { 589 | isa = XCSwiftPackageProductDependency; 590 | package = A389C47228E7542200567CFE /* XCRemoteSwiftPackageReference "Swinject" */; 591 | productName = Swinject; 592 | }; 593 | A3AF32CA28E72E9200E6445E /* NetworkingInterface */ = { 594 | isa = XCSwiftPackageProductDependency; 595 | productName = NetworkingInterface; 596 | }; 597 | A3EE3D8328E70D97004AD9B7 /* CharacterDetails */ = { 598 | isa = XCSwiftPackageProductDependency; 599 | productName = CharacterDetails; 600 | }; 601 | A3EE3D8528E70D9A004AD9B7 /* CharacterList */ = { 602 | isa = XCSwiftPackageProductDependency; 603 | productName = CharacterList; 604 | }; 605 | /* End XCSwiftPackageProductDependency section */ 606 | }; 607 | rootObject = A3EC36FF28E1AF4900329FF4 /* Project object */; 608 | } 609 | -------------------------------------------------------------------------------- /ModularSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ModularSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ModularSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "kingfisher", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/onevcat/Kingfisher.git", 7 | "state" : { 8 | "revision" : "2ef543ee21d63734e1c004ad6c870255e8716c50", 9 | "version" : "7.12.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swinject", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/Swinject/Swinject.git", 16 | "state" : { 17 | "revision" : "11e65d83ba100459a41837119a8af32f182339f1", 18 | "version" : "2.8.2" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /ModularSwiftUI.xcodeproj/xcshareddata/xcschemes/ModularSwiftUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ModularSwiftUI/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 29/09/2022. 6 | // 7 | 8 | import UIKit 9 | import Swinject 10 | 11 | class AppDelegate: NSObject, UIApplicationDelegate { 12 | private let coordinator = Container.AppContainer.resolve(CharacterCoordinator.self)! 13 | lazy var rootView = CharacterCoordinatorView(coordinator: coordinator) 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 16 | coordinator.start() 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ModularSwiftUI/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 | -------------------------------------------------------------------------------- /ModularSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ModularSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Coordinator/CharacterCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 29/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Swinject 10 | import CharacterList 11 | import CharacterDetails 12 | import CharacterModels 13 | 14 | class CharacterCoordinator: ObservableObject, Identifiable, CharacterCoordinating { 15 | @Published var viewModel: CharacterListViewModel? 16 | @Published var detailViewModel: CharacterDetailsViewModel? 17 | 18 | func start() { 19 | viewModel = Container.CharactersListContainer.resolve(CharacterListViewModel.self)! 20 | } 21 | 22 | func open(_ character: Character) { 23 | detailViewModel = Container.CharacterDetailsContainer.resolve(CharacterDetailsViewModel.self, 24 | argument: character) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Coordinator/CharacterCoordinatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterCoordinatorView.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 29/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import CharacterList 10 | import CharacterDetails 11 | 12 | struct CharacterCoordinatorView: View { 13 | @ObservedObject var coordinator: CharacterCoordinator 14 | 15 | var body: some View { 16 | NavigationView { 17 | CharacterList(viewModel: coordinator.viewModel!) 18 | .navigation(item: $coordinator.detailViewModel) { viewModel in 19 | CharacterDetails(viewModel: viewModel) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/DI/AppContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContainer.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 29/09/2022. 6 | // 7 | 8 | import Swinject 9 | import CharacterList 10 | import NetworkingInterface 11 | import Networking 12 | 13 | extension Container { 14 | static let AppContainer: Container = { 15 | let container = Container() 16 | container.register(HTTPClient.self, factory: {_ in URLSessionClient()}).inObjectScope(.container) 17 | container.register(CharacterCoordinating.self, factory: {_ in CharacterCoordinator()}).inObjectScope(.container) 18 | container.register(CharacterCoordinator.self) { $0.resolve(CharacterCoordinating.self)! as! CharacterCoordinator } 19 | return container 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/DI/CharacterDetailsContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterDetailsContainer.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 27/09/2022. 6 | // 7 | 8 | import Swinject 9 | import CharacterDetails 10 | 11 | extension Container { 12 | public static let CharacterDetailsContainer: Container = { 13 | let container = Container() 14 | container.register(CharacterDetailsViewModel.self) { _, character in 15 | CharacterDetailsViewModel(character: character) 16 | } 17 | return container 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/DI/CharactersListContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharactersListContainer.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import Swinject 9 | import CharacterList 10 | import NetworkingInterface 11 | 12 | extension Container { 13 | static let CharactersListContainer: Container = { 14 | let container = Container(parent: AppContainer) 15 | container.register(CharacterListNetworkServicing.self, factory: { resolver in 16 | CharacterListNetworkService(client: resolver.resolve(HTTPClient.self)!) 17 | }) 18 | container.register(CharacterListUseCase.self, factory: { resolver in 19 | CharacterListUseCase(networkService: resolver.resolve(CharacterListNetworkServicing.self)!) 20 | }) 21 | container.register(CharacterListViewModel.self, factory: {resolver in 22 | CharacterListViewModel(characterListUseCase: resolver.resolve(CharacterListUseCase.self)!, 23 | coordinator: resolver.resolve(CharacterCoordinating.self)) 24 | 25 | }) 26 | return container 27 | }() 28 | } 29 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Networking/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Networking/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Networking", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "Networking", 15 | targets: ["Networking"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | .package(path: "../NetworkingInterface") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "Networking", 27 | dependencies: ["NetworkingInterface"]), 28 | .testTarget( 29 | name: "NetworkingTests", 30 | dependencies: ["Networking"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Networking/README.md: -------------------------------------------------------------------------------- 1 | # Networking 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Networking/Sources/Networking/Networking.swift: -------------------------------------------------------------------------------- 1 | public struct Networking { 2 | public private(set) var text = "Hello, World!" 3 | 4 | public init() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Networking/Sources/Networking/URLSessionClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionClient.swift 3 | // BreakingBad 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import NetworkingInterface 11 | 12 | public struct URLSessionClient: HTTPClient { 13 | public init() {} 14 | 15 | public func perform(_ request: URLRequest) -> AnyPublisher, Error> { 16 | return URLSession.shared.dataTaskPublisher(for: request) 17 | .retry(3) 18 | .tryMap{result -> NetworkingResponse in 19 | let item: T = try JSONDecoder().decode(T.self, from: result.data) 20 | return NetworkingResponse(value: item, response: result.response) 21 | } 22 | .receive(on: DispatchQueue.main) 23 | .eraseToAnyPublisher() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Networking/Tests/NetworkingTests/NetworkingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Networking 3 | 4 | final class NetworkingTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "NetworkingInterface", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "NetworkingInterface", 15 | targets: ["NetworkingInterface"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "NetworkingInterface", 26 | dependencies: []), 27 | .testTarget( 28 | name: "NetworkingInterfaceTests", 29 | dependencies: ["NetworkingInterface"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/README.md: -------------------------------------------------------------------------------- 1 | # NetworkingInterface 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/Sources/NetworkingInterface/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol HTTPClient { 5 | func perform(_ request: URLRequest) -> AnyPublisher, Error> 6 | } 7 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/Sources/NetworkingInterface/NetworkingRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mostafa Nafie on 30/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol NetworkingRequest { 11 | var method: HTTPMethod { get } 12 | var path: String { get } 13 | var headers: [String: String] { get } 14 | var body: Data? { get } 15 | 16 | func buildURLRequest() -> URLRequest 17 | } 18 | 19 | public extension NetworkingRequest { 20 | var method: HTTPMethod { .GET } 21 | var body: Data? { nil } 22 | 23 | func buildURLRequest() -> URLRequest { 24 | let baseURL = URL(string: "https://gist.githubusercontent.com/MostafaNafie/2873132c8b48f445d3e383b466966c08/raw/8e948b91043bff6223bafc1aac4b2d6d0f5d04ae")! 25 | let pathURL = baseURL.appendingPathComponent(path) 26 | var urlRequest = URLRequest(url: pathURL) 27 | urlRequest.httpMethod = method.rawValue 28 | urlRequest.allHTTPHeaderFields = headers 29 | urlRequest.httpBody = body 30 | return urlRequest 31 | } 32 | } 33 | 34 | public enum HTTPMethod: String { 35 | case GET 36 | case POST 37 | case PUT 38 | case DELETE 39 | case PATCH 40 | } 41 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/Sources/NetworkingInterface/NetworkingResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingResponse.swift 3 | // 4 | // 5 | // Created by Mostafa Nafie on 30/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct NetworkingResponse { 11 | public init(value: T, response: URLResponse) { 12 | self.value = value 13 | self.response = response 14 | } 15 | 16 | public let value: T 17 | let response: URLResponse 18 | } 19 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/NetworkingInterface/Tests/NetworkingInterfaceTests/NetworkingInterfaceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import NetworkingInterface 3 | 4 | final class NetworkingInterfaceTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(NetworkingInterface().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Utilities", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "Utilities", 15 | targets: ["Utilities"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "Utilities", 26 | dependencies: []), 27 | .testTarget( 28 | name: "UtilitiesTests", 29 | dependencies: ["Utilities"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/README.md: -------------------------------------------------------------------------------- 1 | # Utilities 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/Sources/Utilities/View+Navigation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Navigation.swift 3 | // BreakingBad 4 | // 5 | // Created by Mostafa Nafie on 29/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | 12 | func onNavigation(_ action: @escaping () -> Void) -> some View { 13 | let isActive = Binding( 14 | get: { false }, 15 | set: { newValue in 16 | if newValue { 17 | action() 18 | } 19 | } 20 | ) 21 | return NavigationLink( 22 | destination: EmptyView(), 23 | isActive: isActive 24 | ) { 25 | self 26 | } 27 | } 28 | 29 | func navigation( 30 | item: Binding, 31 | @ViewBuilder destination: (Item) -> Destination 32 | ) -> some View { 33 | let isActive = Binding( 34 | get: { item.wrappedValue != nil }, 35 | set: { value in 36 | if !value { 37 | item.wrappedValue = nil 38 | } 39 | } 40 | ) 41 | return navigation(isActive: isActive) { 42 | item.wrappedValue.map(destination) 43 | } 44 | } 45 | 46 | func navigation( 47 | isActive: Binding, 48 | @ViewBuilder destination: () -> Destination 49 | ) -> some View { 50 | overlay( 51 | NavigationLink( 52 | destination: isActive.wrappedValue ? destination() : nil, 53 | isActive: isActive, 54 | label: { EmptyView() } 55 | ) 56 | ) 57 | } 58 | 59 | } 60 | 61 | extension NavigationLink { 62 | 63 | init(item: Binding, 64 | @ViewBuilder destination: (T) -> D, 65 | @ViewBuilder label: () -> Label) where Destination == D? { 66 | let isActive = Binding( 67 | get: { item.wrappedValue != nil }, 68 | set: { value in 69 | if !value { 70 | item.wrappedValue = nil 71 | } 72 | } 73 | ) 74 | self.init( 75 | destination: item.wrappedValue.map(destination), 76 | isActive: isActive, 77 | label: label 78 | ) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/Sources/Utilities/View+errorAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+errorAlert.swift 3 | // 4 | // 5 | // Created by Mostafa Nafie on 01/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | func errorAlert(error: Binding, buttonTitle: String = "OK") -> some View { 12 | let localizedAlertError = LocalizedAlertError(error: error.wrappedValue) 13 | return alert(isPresented: .constant(localizedAlertError != nil), 14 | error: localizedAlertError) { _ in 15 | Button(buttonTitle) { 16 | error.wrappedValue = nil 17 | } 18 | } message: { error in 19 | Text(error.recoverySuggestion ?? "") 20 | } 21 | } 22 | } 23 | 24 | struct LocalizedAlertError: LocalizedError { 25 | let underlyingError: Error 26 | 27 | var errorDescription: String? { 28 | "\(type(of: underlyingError))" 29 | } 30 | var recoverySuggestion: String? { 31 | underlyingError.localizedDescription 32 | } 33 | 34 | init?(error: Error?) { 35 | guard let error = error else { return nil } 36 | underlyingError = error 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/Sources/Utilities/View+isHidden.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+isHidden.swift 3 | // BreakingBad 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | @ViewBuilder func isHidden(_ isHidden: Bool) -> some View { 12 | if isHidden { 13 | self.hidden() 14 | } else { 15 | self 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/Sources/Utilities/View+onViewDidLoad.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+onViewDidLoad.swift 3 | // BreakingBad 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct ViewDidLoadModifier: ViewModifier { 11 | @State private var viewDidLoad = false 12 | let action: (() -> Void)? 13 | 14 | public func body(content: Content) -> some View { 15 | content 16 | .onAppear { 17 | if viewDidLoad == false { 18 | viewDidLoad = true 19 | action?() 20 | } 21 | } 22 | } 23 | } 24 | 25 | public extension View { 26 | func onViewDidLoad(perform action: (() -> Void)? = nil) -> some View { 27 | self.modifier(ViewDidLoadModifier(action: action)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ModularSwiftUI/Common/Utilities/Tests/UtilitiesTests/UtilitiesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Utilities 3 | 4 | final class UtilitiesTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(Utilities().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ModularSwiftUI/ModularSwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModularSwiftUIApp.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ModularSwiftUIApp: App { 12 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | appDelegate.rootView 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/.swiftpm/xcode/xcshareddata/xcschemes/CharacterDetails.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "kingfisher", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/onevcat/Kingfisher.git", 7 | "state" : { 8 | "revision" : "be1a1acb283a702b99b630f586877ba02234b4cb", 9 | "version" : "7.3.2" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CharacterDetails", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "CharacterDetails", 15 | targets: ["CharacterDetails"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0") 20 | .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.0.0"), 21 | .package(path: "../CharacterModels") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "CharacterDetails", 28 | dependencies: ["Kingfisher", "CharacterModels"]), 29 | .testTarget( 30 | name: "CharacterDetailsTests", 31 | dependencies: ["CharacterDetails"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/README.md: -------------------------------------------------------------------------------- 1 | # CharacterDetails 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/Sources/CharacterDetails/View/CharacterDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterDetails.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 27/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Kingfisher 10 | import CharacterModels 11 | 12 | public struct CharacterDetails: View { 13 | @ObservedObject var viewModel: CharacterDetailsViewModel 14 | 15 | public init(viewModel: CharacterDetailsViewModel) { 16 | self.viewModel = viewModel 17 | } 18 | 19 | public var body: some View { 20 | ScrollView { 21 | KFImage(viewModel.character.imageUrl) 22 | .resizable() 23 | .ignoresSafeArea(edges: .top) 24 | .scaledToFill() 25 | .frame(height: 400, alignment: .top) 26 | .overlay { 27 | Rectangle() 28 | .opacity(0.5) 29 | } 30 | .clipped() 31 | 32 | CircleImage(imageUrl: viewModel.character.imageUrl) 33 | .offset(y: -225) 34 | .padding(.bottom, -280) 35 | 36 | VStack(alignment: .center) { 37 | HStack { 38 | Text(viewModel.character.name) 39 | .font(.title) 40 | } 41 | 42 | Divider() 43 | 44 | Text("About \(viewModel.character.name)") 45 | .font(.title2) 46 | .padding() 47 | Text("Nickname: \(viewModel.character.nickname)") 48 | .font(.body) 49 | Text("Birthday: \(viewModel.character.birthday)") 50 | .font(.body) 51 | } 52 | .padding() 53 | } 54 | .navigationBarTitleDisplayMode(.inline) 55 | } 56 | } 57 | 58 | struct CharacterDetails_Previews: PreviewProvider { 59 | static var previews: some View { 60 | let character = Character(id: 1, name: "Walter White", imageUrl: .init(string: "https://images.amcnetworks.com/amc.com/wp-content/uploads/2015/04/cast_bb_700x1000_walter-white-lg.jpg")!, nickname: "Walter", birthday: "23/23/23") 61 | CharacterDetails(viewModel: .init(character: character)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/Sources/CharacterDetails/View/CircleImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleImage.swift 3 | // Landmarks 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Kingfisher 10 | 11 | struct CircleImage: View { 12 | var imageUrl: URL 13 | 14 | var body: some View { 15 | KFImage(imageUrl) 16 | .resizable() 17 | .scaledToFill() 18 | .frame(width: 300, alignment: .top) 19 | .clipShape(Circle()) 20 | .overlay { 21 | Circle().stroke(.white, lineWidth: 4) 22 | } 23 | .shadow(radius: 7) 24 | } 25 | } 26 | 27 | struct CircleImage_Previews: PreviewProvider { 28 | static var previews: some View { 29 | CircleImage(imageUrl: .init(string: "https://images.amcnetworks.com/amc.com/wp-content/uploads/2015/04/cast_bb_700x1000_walter-white-lg.jpg")!) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/Sources/CharacterDetails/ViewModel/CharacterDetailsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterDetailsViewModel.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 27/09/2022. 6 | // 7 | 8 | import Foundation 9 | import CharacterModels 10 | 11 | public final class CharacterDetailsViewModel: ObservableObject { 12 | @Published var character: Character 13 | 14 | public init(character: Character) { 15 | self.character = character 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterDetails/Tests/CharacterDetailsTests/CharacterDetailsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CharacterDetails 3 | 4 | final class CharacterDetailsTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(CharacterDetails().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/.swiftpm/xcode/xcshareddata/xcschemes/CharacterList.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "kingfisher", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/onevcat/Kingfisher.git", 7 | "state" : { 8 | "revision" : "be1a1acb283a702b99b630f586877ba02234b4cb", 9 | "version" : "7.3.2" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CharacterList", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "CharacterList", 15 | targets: ["CharacterList"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.0.0"), 21 | .package(path: "../../Common/Network/NetworkingInterface"), 22 | .package(path: "../../Common/Utilities"), 23 | .package(path: "../CharacterModels") 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 27 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 28 | .target( 29 | name: "CharacterList", 30 | dependencies: ["Kingfisher", "NetworkingInterface", "Utilities", "CharacterModels"]), 31 | .testTarget( 32 | name: "CharacterListTests", 33 | dependencies: ["CharacterList"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/README.md: -------------------------------------------------------------------------------- 1 | # CharacterList 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/Networking/CharacterListNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterListNetworkService.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import NetworkingInterface 11 | 12 | public protocol CharacterListNetworkServicing { 13 | typealias CharactersResponse = [CharactersNetwork.Character] 14 | 15 | func fetchCharacters() -> AnyPublisher 16 | } 17 | 18 | public struct CharacterListNetworkService: CharacterListNetworkServicing { 19 | private let client: HTTPClient 20 | 21 | public init(client: HTTPClient) { 22 | self.client = client 23 | } 24 | 25 | public func fetchCharacters() -> AnyPublisher { 26 | let request = CharactersRequest().buildURLRequest() 27 | return client.perform(request) 28 | .map(\.value) 29 | .eraseToAnyPublisher() 30 | } 31 | } 32 | 33 | class PreviewCharacterListNetworkService: CharacterListNetworkServicing { 34 | func fetchCharacters() -> AnyPublisher { 35 | let fakeCharacters: CharactersResponse = [ 36 | .init(id: 1, name: "Walter White", birthday: "", occupation: nil, img: "https://vignette.wikia.nocookie.net/breakingbad/images/c/c1/4x11_-_Huell.png/revision/latest?cb=20130913100459&path-prefix=es", status: nil, nickname: "", appearance: nil, portrayed: nil, category: nil, betterCallSaulAppearance: nil), 37 | .init(id: 2, name: "Jesse Pinkman", birthday: "", occupation: nil, img: "https://vignette.wikia.nocookie.net/breakingbad/images/c/c1/4x11_-_Huell.png/revision/latest?cb=20130913100459&path-prefix=es", status: nil, nickname: "", appearance: nil, portrayed: nil, category: nil, betterCallSaulAppearance: nil), 38 | .init(id: 3, name: "Henry Schrader", birthday: "", occupation: nil, img: "https://vignette.wikia.nocookie.net/breakingbad/images/c/c1/4x11_-_Huell.png/revision/latest?cb=20130913100459&path-prefix=es", status: nil, nickname: "", appearance: nil, portrayed: nil, category: nil, betterCallSaulAppearance: nil) 39 | ] 40 | return Just(fakeCharacters) 41 | .setFailureType(to: Error.self) 42 | .eraseToAnyPublisher() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/Networking/CharactersRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharactersRequest.swift 3 | // 4 | // 5 | // Created by Mostafa Nafie on 30/09/2022. 6 | // 7 | 8 | import Foundation 9 | import NetworkingInterface 10 | 11 | struct CharactersRequest: NetworkingRequest { 12 | var path: String { "/BreakingBadCharacters" } 13 | var headers: [String: String] = [:] 14 | } 15 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/Networking/CharactersResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataModel.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Character 11 | public enum CharactersNetwork { 12 | public struct Character: Codable { 13 | let id: Int 14 | let name: String 15 | let birthday: String 16 | let occupation: [String]? 17 | let img: String 18 | let status: Status? 19 | let nickname: String 20 | let appearance: [Int]? 21 | let portrayed: String? 22 | let category: Category? 23 | let betterCallSaulAppearance: [Int]? 24 | 25 | enum CodingKeys: String, CodingKey { 26 | case id = "char_id" 27 | case name, birthday, occupation, img, status, nickname, appearance, portrayed, category 28 | case betterCallSaulAppearance = "better_call_saul_appearance" 29 | } 30 | } 31 | 32 | enum Category: String, Codable { 33 | case betterCallSaul = "Better Call Saul" 34 | case breakingBad = "Breaking Bad" 35 | case breakingBadBetterCallSaul = "Breaking Bad, Better Call Saul" 36 | } 37 | 38 | enum Status: String, Codable { 39 | case alive = "Alive" 40 | case deceased = "Deceased" 41 | case presumedDead = "Presumed dead" 42 | case unknown = "Unknown" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/UseCase/CharacterListUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharactersListUseCase.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 01/10/2022. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import CharacterModels 11 | 12 | public struct CharacterListUseCase { 13 | private let networkService: CharacterListNetworkServicing 14 | 15 | public init(networkService: CharacterListNetworkServicing) { 16 | self.networkService = networkService 17 | } 18 | 19 | func fetchCharacters() -> AnyPublisher<[Character], Error> { 20 | networkService.fetchCharacters() 21 | .print(#function) 22 | .map{ charactersResponse in 23 | charactersResponse.map { 24 | Character(id: $0.id, 25 | name: $0.name, 26 | imageUrl: URL(string: $0.img)!, 27 | nickname: $0.nickname, 28 | birthday: $0.birthday) 29 | } 30 | } 31 | .eraseToAnyPublisher() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/View/Caption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Caption.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Caption: View { 11 | let text: String 12 | 13 | var body: some View { 14 | Text(text) 15 | .fontWeight(.semibold) 16 | .padding(5) 17 | .background(.white.opacity(0.75), 18 | in: RoundedRectangle(cornerRadius: 10.0, style: .continuous)) 19 | .padding(5) 20 | } 21 | } 22 | 23 | struct Caption_Previews: PreviewProvider { 24 | static var previews: some View { 25 | Caption(text: "Walter White") 26 | .background(.black) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/View/CharacterList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterList.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Utilities 10 | 11 | public struct CharacterList: View { 12 | @ObservedObject private var viewModel: CharacterListViewModel 13 | 14 | public init(viewModel: CharacterListViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | public var body: some View { 19 | List($viewModel.filteredCharacters) { character in 20 | CharacterRow(character: character.wrappedValue) 21 | .listRowSeparator(.hidden) 22 | .onNavigation { 23 | viewModel.open(character.wrappedValue) 24 | } 25 | } 26 | .listStyle(PlainListStyle()) 27 | .navigationTitle("Breaking Bad") 28 | .onViewDidLoad { 29 | viewModel.viewDidLoad() 30 | } 31 | .searchable(text: $viewModel.searchQuery, prompt: "Search Characters by Name") { 32 | ForEach($viewModel.filteredCharacters) { character in 33 | Text(character.wrappedValue.name) 34 | .searchCompletion(character.wrappedValue.name) 35 | } 36 | } 37 | .onChange(of: viewModel.searchQuery) { query in 38 | viewModel.searchQuery = query 39 | } 40 | .errorAlert(error: $viewModel.error) 41 | } 42 | } 43 | 44 | struct CharacterList_Previews: PreviewProvider { 45 | static let viewModel = CharacterListViewModel(characterListUseCase: .init(networkService: PreviewCharacterListNetworkService())) 46 | static var previews: some View { 47 | CharacterList(viewModel: viewModel) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/View/CharacterRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterRow.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Kingfisher 10 | import CharacterModels 11 | 12 | struct CharacterRow: View { 13 | let character: Character 14 | 15 | @State private var isAnimating = true 16 | 17 | var body: some View { 18 | KFImage(character.imageUrl) 19 | .fade(duration: 0.35) 20 | .onSuccess { _ in 21 | isAnimating = false 22 | } 23 | .resizable() 24 | .scaledToFill() 25 | .frame(height: 400, alignment: .top) 26 | .clipped() 27 | .overlay(alignment: .bottom) { 28 | Caption(text: character.name) 29 | .opacity(0.75) 30 | } 31 | .overlay(alignment: .center) { 32 | ProgressView() 33 | .isHidden(!isAnimating) 34 | } 35 | .cornerRadius(14) 36 | } 37 | 38 | } 39 | 40 | struct CharacterRow_Previews: PreviewProvider { 41 | static var previews: some View { 42 | CharacterRow(character: .init(id: 1, name: "Walter White", imageUrl: URL(string: "https://vignette.wikia.nocookie.net/breakingbad/images/c/c1/4x11_-_Huell.png/revision/latest?cb=20130913100459&path-prefix=es")!, nickname: "Walter", birthday: "23/23/23")) 43 | .previewLayout(.fixed(width: 300, height: 200)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Sources/CharacterList/ViewModel/CharacterListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterListViewModel.swift 3 | // ModularSwiftUI 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import CharacterModels 11 | 12 | public protocol CharacterCoordinating: AnyObject { 13 | func open(_ character: Character) 14 | } 15 | 16 | public final class CharacterListViewModel: ObservableObject { 17 | @Published var filteredCharacters: [Character] = [] 18 | @Published var searchQuery: String = "" 19 | @Published var error: Error? 20 | 21 | @Published private var characters: [Character] = [] 22 | private let characterListUseCase: CharacterListUseCase 23 | private let coordinator: CharacterCoordinating? 24 | private var cancellables: Set = [] 25 | 26 | public init(characterListUseCase: CharacterListUseCase, 27 | coordinator: CharacterCoordinating? = nil) { 28 | self.characterListUseCase = characterListUseCase 29 | self.coordinator = coordinator 30 | 31 | setupBindings() 32 | } 33 | 34 | func viewDidLoad() { 35 | fetchCharacters() 36 | } 37 | 38 | func open(_ character: Character) { 39 | coordinator?.open(character) 40 | } 41 | } 42 | 43 | private extension CharacterListViewModel { 44 | func setupBindings() { 45 | $searchQuery 46 | .print("SearchQuery") 47 | .combineLatest($characters) 48 | .map { (searchQuery, characters) in 49 | characters.filter { 50 | searchQuery.isEmpty ? true : $0.name 51 | .localizedCaseInsensitiveContains(searchQuery) 52 | } 53 | } 54 | .assign(to: &$filteredCharacters) 55 | } 56 | 57 | func fetchCharacters() { 58 | characterListUseCase 59 | .fetchCharacters() 60 | .sink(receiveCompletion: { [weak self] completion in 61 | switch completion { 62 | case .failure(let error): 63 | self?.error = error 64 | case .finished: 65 | print("Success: \(#function)") 66 | } 67 | }, receiveValue: { [weak self] value in 68 | self?.characters = value 69 | }) 70 | .store(in: &cancellables) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Tests/CharacterListTests/CharacterListViewModelDoubles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterListViewModelDoubles.swift 3 | // ModularSwiftUITests 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | @testable import CharacterList 11 | 12 | class MockCharacterListNetworkService: CharacterListNetworkServicing { 13 | func fetchCharacters() -> AnyPublisher { 14 | let fakeCharacters: CharactersResponse = [ 15 | .init(id: 1, name: "Walter White", birthday: "", occupation: nil, img: "google.com", status: nil, nickname: "", appearance: nil, portrayed: nil, category: nil, betterCallSaulAppearance: nil), 16 | .init(id: 2, name: "Jesse Pinkman", birthday: "", occupation: nil, img: "google.com", status: nil, nickname: "", appearance: nil, portrayed: nil, category: nil, betterCallSaulAppearance: nil), 17 | .init(id: 3, name: "Henry Schrader", birthday: "", occupation: nil, img: "google.com", status: nil, nickname: "", appearance: nil, portrayed: nil, category: nil, betterCallSaulAppearance: nil) 18 | ] 19 | return Just(fakeCharacters) 20 | .setFailureType(to: Error.self) 21 | .eraseToAnyPublisher() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterList/Tests/CharacterListTests/CharacterListViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterListViewModelTests.swift 3 | // ModularSwiftUITests 4 | // 5 | // Created by Mostafa Nafie on 26/09/2022. 6 | // 7 | 8 | import XCTest 9 | import SwiftUI 10 | @testable import CharacterList 11 | 12 | class CharacterListViewModelTests: XCTestCase { 13 | private let characterListUseCase = CharacterListUseCase(networkService: MockCharacterListNetworkService()) 14 | private var sut: CharacterListViewModel! 15 | 16 | override func setUp() { 17 | self.sut = CharacterListViewModel(characterListUseCase: characterListUseCase) 18 | } 19 | 20 | override func tearDown() { 21 | self.sut = nil 22 | } 23 | 24 | func test_returns_correct_initial_state() { 25 | let expectedValue = 0 26 | let actualValue = sut.filteredCharacters.count 27 | 28 | XCTAssertEqual(expectedValue, actualValue) 29 | } 30 | 31 | func test_returns_correct_fetched_characters_count() { 32 | sut.viewDidLoad() 33 | 34 | let expectedValue = 3 35 | let actualValue = sut.filteredCharacters.count 36 | 37 | XCTAssertEqual(expectedValue, actualValue) 38 | } 39 | 40 | func test_returns_correct_fetched_characters_mapping() { 41 | sut.viewDidLoad() 42 | 43 | let expectedValue = "Walter White" 44 | let actualValue = sut.filteredCharacters.first?.name 45 | 46 | XCTAssertEqual(expectedValue, actualValue) 47 | } 48 | 49 | func test_returns_correct_filtered_characters() { 50 | sut.viewDidLoad() 51 | sut.searchQuery = "wh" 52 | 53 | var expectedValue = "Walter White" 54 | var actualValue = sut.filteredCharacters.first?.name 55 | XCTAssertEqual(expectedValue, actualValue) 56 | 57 | sut.searchQuery = "jesse" 58 | 59 | expectedValue = "Jesse Pinkman" 60 | actualValue = sut.filteredCharacters.first?.name 61 | XCTAssertEqual(expectedValue, actualValue) 62 | } 63 | 64 | func test_returns_correct_unfiltered_characters_after_reset() { 65 | sut.viewDidLoad() 66 | sut.searchQuery = "wh" 67 | 68 | var expectedValue = 1 69 | var actualValue = sut.filteredCharacters.count 70 | XCTAssertEqual(expectedValue, actualValue) 71 | 72 | sut.searchQuery = "" 73 | 74 | expectedValue = 3 75 | actualValue = sut.filteredCharacters.count 76 | XCTAssertEqual(expectedValue, actualValue) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterModels/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterModels/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CharacterModels", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "CharacterModels", 12 | targets: ["CharacterModels"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "CharacterModels", 23 | dependencies: []), 24 | .testTarget( 25 | name: "CharacterModelsTests", 26 | dependencies: ["CharacterModels"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterModels/README.md: -------------------------------------------------------------------------------- 1 | # CharacterModels 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterModels/Sources/CharacterModels/CharacterModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterModels.swift 3 | // BreakingBad 4 | // 5 | // Created by Mostafa Nafie on 30/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Character: Identifiable { 11 | public init(id: Int, name: String, imageUrl: URL, nickname: String, birthday: String) { 12 | self.id = id 13 | self.name = name 14 | self.imageUrl = imageUrl 15 | self.nickname = nickname 16 | self.birthday = birthday 17 | } 18 | 19 | public let id: Int 20 | public let name: String 21 | public let imageUrl: URL 22 | public let nickname: String 23 | public let birthday: String 24 | } 25 | -------------------------------------------------------------------------------- /ModularSwiftUI/Modules/CharacterModels/Tests/CharacterModelsTests/CharacterModelsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CharacterModels 3 | 4 | final class CharacterModelsTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(CharacterModels().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ModularSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI+MVVM+DI Demo 2 | 3 | Light | Dark 4 | :-: | :-: 5 |