├── .gitignore ├── CleanArchitecture-MVVM-C-SwiftUI.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CleanArchitecture-MVVM-C-SwiftUI ├── App │ ├── AppDelegate+Injected.swift │ ├── CleanArchitecture_MVVM_C_SwiftUIApp.swift │ └── MainCoordinator.swift ├── Common │ └── Common.swift ├── Domain │ ├── Entities │ │ └── GithubRepoModel.swift │ └── UseCases │ │ └── GithubRepoUseCase.swift ├── Extensions │ ├── Binding+Ext.swift │ ├── ProgressHUD+Ext.swift │ ├── Publisher+Ext.swift │ └── View+Ext.swift ├── Platform │ ├── Repositories │ │ └── GithubRepoRepository.swift │ ├── Routers │ │ └── SearchRepoAPIRouter.swift │ └── Services │ │ ├── APIError.swift │ │ ├── APIInputBase.swift │ │ └── APIService.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ └── Assets │ │ └── Assets.xcassets │ │ ├── AccentColor.colorset │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ └── Contents.json │ │ └── Contents.json └── Scenes │ ├── DetailRepo │ ├── DetailRepoView.swift │ └── DetailRepoViewModel.swift │ └── SearchRepo │ ├── RepoRow.swift │ ├── SearchRepoCoordinator.swift │ ├── SearchRepoView.swift │ └── SearchRepoViewModel.swift ├── CleanArchitecture-MVVM-C-SwiftUITests └── CleanArchitecture_MVVM_C_SwiftUITests.swift ├── CleanArchitecture-MVVM-C-SwiftUIUITests ├── CleanArchitecture_MVVM_C_SwiftUIUITests.swift └── CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,objective-c,cocoapods,swiftpackagemanager,carthage 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,objective-c,cocoapods,swiftpackagemanager,carthage 4 | 5 | ### Carthage ### 6 | # Carthage 7 | # 8 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 9 | # Carthage/Checkouts 10 | 11 | Carthage/Build 12 | 13 | ### CocoaPods ### 14 | ## CocoaPods GitIgnore Template 15 | 16 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 17 | # - Also handy if you have a large number of dependant pods 18 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 19 | Pods/ 20 | 21 | ### Objective-C ### 22 | # Xcode 23 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 24 | 25 | ## User settings 26 | xcuserdata/ 27 | 28 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 29 | *.xcscmblueprint 30 | *.xccheckout 31 | 32 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 33 | build/ 34 | DerivedData/ 35 | *.moved-aside 36 | *.pbxuser 37 | !default.pbxuser 38 | *.mode1v3 39 | !default.mode1v3 40 | *.mode2v3 41 | !default.mode2v3 42 | *.perspectivev3 43 | !default.perspectivev3 44 | 45 | ## Obj-C/Swift specific 46 | *.hmap 47 | 48 | ## App packaging 49 | *.ipa 50 | *.zip 51 | 52 | # CocoaPods 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # Pods/ 57 | # Add this line if you want to avoid checking in source code from the Xcode workspace 58 | # *.xcworkspace 59 | 60 | # Carthage 61 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 62 | # Carthage/Checkouts 63 | 64 | Carthage/Build/ 65 | 66 | # fastlane 67 | # It is recommended to not store the screenshots in the git repo. 68 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | # Code Injection 78 | # After new code Injection tools there's a generated folder /iOSInjectionProject 79 | # https://github.com/johnno1962/injectionforxcode 80 | 81 | iOSInjectionProject/ 82 | 83 | ### Objective-C Patch ### 84 | 85 | ### Swift ### 86 | # Xcode 87 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 88 | 89 | 90 | 91 | 92 | 93 | 94 | ## Playgrounds 95 | timeline.xctimeline 96 | playground.xcworkspace 97 | 98 | # Swift Package Manager 99 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 100 | # Packages/ 101 | # Package.pins 102 | # Package.resolved 103 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 104 | # hence it is not needed unless you have added a package configuration file to your project 105 | # .swiftpm 106 | 107 | .build/ 108 | 109 | # CocoaPods 110 | # We recommend against adding the Pods directory to your .gitignore. However 111 | # you should judge for yourself, the pros and cons are mentioned at: 112 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 113 | # Pods/ 114 | # Add this line if you want to avoid checking in source code from the Xcode workspace 115 | # *.xcworkspace 116 | 117 | # Carthage 118 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 119 | # Carthage/Checkouts 120 | 121 | 122 | # Accio dependency management 123 | Dependencies/ 124 | .accio/ 125 | 126 | # fastlane 127 | # It is recommended to not store the screenshots in the git repo. 128 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 129 | # For more information about the recommended setup visit: 130 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 131 | 132 | 133 | # Code Injection 134 | # After new code Injection tools there's a generated folder /iOSInjectionProject 135 | # https://github.com/johnno1962/injectionforxcode 136 | 137 | 138 | ### SwiftPackageManager ### 139 | Packages 140 | xcuserdata 141 | 142 | 143 | ### Xcode ### 144 | 145 | ## Xcode 8 and earlier 146 | 147 | ### Xcode Patch ### 148 | # !*.xcodeproj/xcshareddata/ 149 | !*.xcworkspace/contents.xcworkspacedata 150 | /*.gcno 151 | **/xcshareddata/WorkspaceSettings.xcsettings 152 | 153 | # End of https://www.toptal.com/developers/gitignore/api/xcode,swift,objective-c,cocoapods,swiftpackagemanager,carthage 154 | Package.resolved 155 | 156 | ### Xcode Gen ### 157 | # *.xcodeproj 158 | builds/xU8AMzz5/0/scm-ai/scm-ai-ios.tmp/git-template 159 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C3DC243F2A21FB0000341D68 /* CleanArchitecture_MVVM_C_SwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC243E2A21FB0000341D68 /* CleanArchitecture_MVVM_C_SwiftUIApp.swift */; }; 11 | C3DC24432A21FB0100341D68 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C3DC24422A21FB0100341D68 /* Assets.xcassets */; }; 12 | C3DC24462A21FB0100341D68 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C3DC24452A21FB0100341D68 /* Preview Assets.xcassets */; }; 13 | C3DC24502A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC244F2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUITests.swift */; }; 14 | C3DC245A2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24592A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITests.swift */; }; 15 | C3DC245C2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC245B2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift */; }; 16 | C3DC246A2A21FB3700341D68 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = C3DC24692A21FB3700341D68 /* Stinsen */; }; 17 | C3DC24702A21FB4B00341D68 /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = C3DC246F2A21FB4B00341D68 /* SwiftMessages */; }; 18 | C3DC24732A21FB5500341D68 /* ObjectMapper in Frameworks */ = {isa = PBXBuildFile; productRef = C3DC24722A21FB5500341D68 /* ObjectMapper */; }; 19 | C3DC24762A21FB5D00341D68 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = C3DC24752A21FB5D00341D68 /* Alamofire */; }; 20 | C3DC24792A21FB7900341D68 /* Factory in Frameworks */ = {isa = PBXBuildFile; productRef = C3DC24782A21FB7900341D68 /* Factory */; }; 21 | C3DC24842A21FC2C00341D68 /* GithubRepoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24832A21FC2C00341D68 /* GithubRepoModel.swift */; }; 22 | C3DC24862A21FC7900341D68 /* GithubRepoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24852A21FC7900341D68 /* GithubRepoUseCase.swift */; }; 23 | C3DC24882A21FC8600341D68 /* GithubRepoRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24872A21FC8600341D68 /* GithubRepoRepository.swift */; }; 24 | C3DC248E2A21FCD600341D68 /* SearchRepoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC248B2A21FCD600341D68 /* SearchRepoCoordinator.swift */; }; 25 | C3DC248F2A21FCD600341D68 /* SearchRepoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC248C2A21FCD600341D68 /* SearchRepoView.swift */; }; 26 | C3DC24902A21FCD600341D68 /* SearchRepoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC248D2A21FCD600341D68 /* SearchRepoViewModel.swift */; }; 27 | C3DC24932A21FD0100341D68 /* View+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24922A21FD0100341D68 /* View+Ext.swift */; }; 28 | C3DC24952A21FD2400341D68 /* Publisher+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24942A21FD2400341D68 /* Publisher+Ext.swift */; }; 29 | C3DC24972A21FD4000341D68 /* Binding+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24962A21FD4000341D68 /* Binding+Ext.swift */; }; 30 | C3DC249A2A21FD6100341D68 /* ProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = C3DC24992A21FD6100341D68 /* ProgressHUD */; }; 31 | C3DC249C2A21FDB500341D68 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC249B2A21FDB500341D68 /* MainCoordinator.swift */; }; 32 | C3DC249F2A21FE7600341D68 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC249E2A21FE7600341D68 /* Common.swift */; }; 33 | C3DC24A12A21FEB200341D68 /* ProgressHUD+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24A02A21FEB200341D68 /* ProgressHUD+Ext.swift */; }; 34 | C3DC24A32A21FEF400341D68 /* APIInputBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24A22A21FEF400341D68 /* APIInputBase.swift */; }; 35 | C3DC24A52A21FEFD00341D68 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24A42A21FEFD00341D68 /* APIError.swift */; }; 36 | C3DC24A72A21FF1500341D68 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24A62A21FF1500341D68 /* APIService.swift */; }; 37 | C3DC24AA2A21FFF100341D68 /* SearchRepoAPIRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24A92A21FFF100341D68 /* SearchRepoAPIRouter.swift */; }; 38 | C3DC24AC2A22011900341D68 /* AppDelegate+Injected.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24AB2A22011900341D68 /* AppDelegate+Injected.swift */; }; 39 | C3DC24B22A22048300341D68 /* DetailRepoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24AF2A22048300341D68 /* DetailRepoView.swift */; }; 40 | C3DC24B32A22048300341D68 /* DetailRepoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24B02A22048300341D68 /* DetailRepoViewModel.swift */; }; 41 | C3DC24B72A22079100341D68 /* RepoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DC24B62A22079100341D68 /* RepoRow.swift */; }; 42 | /* End PBXBuildFile section */ 43 | 44 | /* Begin PBXContainerItemProxy section */ 45 | C3DC244C2A21FB0100341D68 /* PBXContainerItemProxy */ = { 46 | isa = PBXContainerItemProxy; 47 | containerPortal = C3DC24332A21FB0000341D68 /* Project object */; 48 | proxyType = 1; 49 | remoteGlobalIDString = C3DC243A2A21FB0000341D68; 50 | remoteInfo = "CleanArchitecture-MVVM-C-SwiftUI"; 51 | }; 52 | C3DC24562A21FB0100341D68 /* PBXContainerItemProxy */ = { 53 | isa = PBXContainerItemProxy; 54 | containerPortal = C3DC24332A21FB0000341D68 /* Project object */; 55 | proxyType = 1; 56 | remoteGlobalIDString = C3DC243A2A21FB0000341D68; 57 | remoteInfo = "CleanArchitecture-MVVM-C-SwiftUI"; 58 | }; 59 | /* End PBXContainerItemProxy section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | C3DC243B2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CleanArchitecture-MVVM-C-SwiftUI.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | C3DC243E2A21FB0000341D68 /* CleanArchitecture_MVVM_C_SwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanArchitecture_MVVM_C_SwiftUIApp.swift; sourceTree = ""; }; 64 | C3DC24422A21FB0100341D68 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 65 | C3DC24452A21FB0100341D68 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 66 | C3DC244B2A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CleanArchitecture-MVVM-C-SwiftUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | C3DC244F2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanArchitecture_MVVM_C_SwiftUITests.swift; sourceTree = ""; }; 68 | C3DC24552A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CleanArchitecture-MVVM-C-SwiftUIUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | C3DC24592A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanArchitecture_MVVM_C_SwiftUIUITests.swift; sourceTree = ""; }; 70 | C3DC245B2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift; sourceTree = ""; }; 71 | C3DC24832A21FC2C00341D68 /* GithubRepoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepoModel.swift; sourceTree = ""; }; 72 | C3DC24852A21FC7900341D68 /* GithubRepoUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepoUseCase.swift; sourceTree = ""; }; 73 | C3DC24872A21FC8600341D68 /* GithubRepoRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepoRepository.swift; sourceTree = ""; }; 74 | C3DC248B2A21FCD600341D68 /* SearchRepoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRepoCoordinator.swift; sourceTree = ""; }; 75 | C3DC248C2A21FCD600341D68 /* SearchRepoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRepoView.swift; sourceTree = ""; }; 76 | C3DC248D2A21FCD600341D68 /* SearchRepoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRepoViewModel.swift; sourceTree = ""; }; 77 | C3DC24922A21FD0100341D68 /* View+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Ext.swift"; sourceTree = ""; }; 78 | C3DC24942A21FD2400341D68 /* Publisher+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Ext.swift"; sourceTree = ""; }; 79 | C3DC24962A21FD4000341D68 /* Binding+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Ext.swift"; sourceTree = ""; }; 80 | C3DC249B2A21FDB500341D68 /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; 81 | C3DC249E2A21FE7600341D68 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; 82 | C3DC24A02A21FEB200341D68 /* ProgressHUD+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProgressHUD+Ext.swift"; sourceTree = ""; }; 83 | C3DC24A22A21FEF400341D68 /* APIInputBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIInputBase.swift; sourceTree = ""; }; 84 | C3DC24A42A21FEFD00341D68 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; 85 | C3DC24A62A21FF1500341D68 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 86 | C3DC24A92A21FFF100341D68 /* SearchRepoAPIRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRepoAPIRouter.swift; sourceTree = ""; }; 87 | C3DC24AB2A22011900341D68 /* AppDelegate+Injected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Injected.swift"; sourceTree = ""; }; 88 | C3DC24AF2A22048300341D68 /* DetailRepoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailRepoView.swift; sourceTree = ""; }; 89 | C3DC24B02A22048300341D68 /* DetailRepoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailRepoViewModel.swift; sourceTree = ""; }; 90 | C3DC24B62A22079100341D68 /* RepoRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoRow.swift; sourceTree = ""; }; 91 | /* End PBXFileReference section */ 92 | 93 | /* Begin PBXFrameworksBuildPhase section */ 94 | C3DC24382A21FB0000341D68 /* Frameworks */ = { 95 | isa = PBXFrameworksBuildPhase; 96 | buildActionMask = 2147483647; 97 | files = ( 98 | C3DC24762A21FB5D00341D68 /* Alamofire in Frameworks */, 99 | C3DC249A2A21FD6100341D68 /* ProgressHUD in Frameworks */, 100 | C3DC24732A21FB5500341D68 /* ObjectMapper in Frameworks */, 101 | C3DC24792A21FB7900341D68 /* Factory in Frameworks */, 102 | C3DC24702A21FB4B00341D68 /* SwiftMessages in Frameworks */, 103 | C3DC246A2A21FB3700341D68 /* Stinsen in Frameworks */, 104 | ); 105 | runOnlyForDeploymentPostprocessing = 0; 106 | }; 107 | C3DC24482A21FB0100341D68 /* Frameworks */ = { 108 | isa = PBXFrameworksBuildPhase; 109 | buildActionMask = 2147483647; 110 | files = ( 111 | ); 112 | runOnlyForDeploymentPostprocessing = 0; 113 | }; 114 | C3DC24522A21FB0100341D68 /* Frameworks */ = { 115 | isa = PBXFrameworksBuildPhase; 116 | buildActionMask = 2147483647; 117 | files = ( 118 | ); 119 | runOnlyForDeploymentPostprocessing = 0; 120 | }; 121 | /* End PBXFrameworksBuildPhase section */ 122 | 123 | /* Begin PBXGroup section */ 124 | C3DC24322A21FB0000341D68 = { 125 | isa = PBXGroup; 126 | children = ( 127 | C3DC243D2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI */, 128 | C3DC244E2A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUITests */, 129 | C3DC24582A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUIUITests */, 130 | C3DC243C2A21FB0000341D68 /* Products */, 131 | ); 132 | sourceTree = ""; 133 | }; 134 | C3DC243C2A21FB0000341D68 /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | C3DC243B2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI.app */, 138 | C3DC244B2A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUITests.xctest */, 139 | C3DC24552A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUIUITests.xctest */, 140 | ); 141 | name = Products; 142 | sourceTree = ""; 143 | }; 144 | C3DC243D2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | C3DC24802A21FBB500341D68 /* App */, 148 | C3DC24822A21FC0600341D68 /* Scenes */, 149 | C3DC247A2A21FB9600341D68 /* Platform */, 150 | C3DC247D2A21FBA600341D68 /* Domain */, 151 | C3DC249D2A21FE6E00341D68 /* Common */, 152 | C3DC24892A21FC9000341D68 /* Extensions */, 153 | C3DC24812A21FBC300341D68 /* Resources */, 154 | C3DC24442A21FB0100341D68 /* Preview Content */, 155 | ); 156 | path = "CleanArchitecture-MVVM-C-SwiftUI"; 157 | sourceTree = ""; 158 | }; 159 | C3DC24442A21FB0100341D68 /* Preview Content */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | C3DC24452A21FB0100341D68 /* Preview Assets.xcassets */, 163 | ); 164 | path = "Preview Content"; 165 | sourceTree = ""; 166 | }; 167 | C3DC244E2A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUITests */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | C3DC244F2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUITests.swift */, 171 | ); 172 | path = "CleanArchitecture-MVVM-C-SwiftUITests"; 173 | sourceTree = ""; 174 | }; 175 | C3DC24582A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUIUITests */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | C3DC24592A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITests.swift */, 179 | C3DC245B2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift */, 180 | ); 181 | path = "CleanArchitecture-MVVM-C-SwiftUIUITests"; 182 | sourceTree = ""; 183 | }; 184 | C3DC247A2A21FB9600341D68 /* Platform */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | C3DC24A82A21FFD900341D68 /* Routers */, 188 | C3DC247C2A21FBA100341D68 /* Services */, 189 | C3DC247B2A21FB9B00341D68 /* Repositories */, 190 | ); 191 | path = Platform; 192 | sourceTree = ""; 193 | }; 194 | C3DC247B2A21FB9B00341D68 /* Repositories */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | C3DC24872A21FC8600341D68 /* GithubRepoRepository.swift */, 198 | ); 199 | path = Repositories; 200 | sourceTree = ""; 201 | }; 202 | C3DC247C2A21FBA100341D68 /* Services */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | C3DC24A22A21FEF400341D68 /* APIInputBase.swift */, 206 | C3DC24A42A21FEFD00341D68 /* APIError.swift */, 207 | C3DC24A62A21FF1500341D68 /* APIService.swift */, 208 | ); 209 | path = Services; 210 | sourceTree = ""; 211 | }; 212 | C3DC247D2A21FBA600341D68 /* Domain */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | C3DC247F2A21FBB000341D68 /* UseCases */, 216 | C3DC247E2A21FBAA00341D68 /* Entities */, 217 | ); 218 | path = Domain; 219 | sourceTree = ""; 220 | }; 221 | C3DC247E2A21FBAA00341D68 /* Entities */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | C3DC24832A21FC2C00341D68 /* GithubRepoModel.swift */, 225 | ); 226 | path = Entities; 227 | sourceTree = ""; 228 | }; 229 | C3DC247F2A21FBB000341D68 /* UseCases */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | C3DC24852A21FC7900341D68 /* GithubRepoUseCase.swift */, 233 | ); 234 | path = UseCases; 235 | sourceTree = ""; 236 | }; 237 | C3DC24802A21FBB500341D68 /* App */ = { 238 | isa = PBXGroup; 239 | children = ( 240 | C3DC243E2A21FB0000341D68 /* CleanArchitecture_MVVM_C_SwiftUIApp.swift */, 241 | C3DC249B2A21FDB500341D68 /* MainCoordinator.swift */, 242 | C3DC24AB2A22011900341D68 /* AppDelegate+Injected.swift */, 243 | ); 244 | path = App; 245 | sourceTree = ""; 246 | }; 247 | C3DC24812A21FBC300341D68 /* Resources */ = { 248 | isa = PBXGroup; 249 | children = ( 250 | C3DC24912A21FCE600341D68 /* Assets */, 251 | ); 252 | path = Resources; 253 | sourceTree = ""; 254 | }; 255 | C3DC24822A21FC0600341D68 /* Scenes */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | C3DC24AD2A22046D00341D68 /* DetailRepo */, 259 | C3DC248A2A21FCB600341D68 /* SearchRepo */, 260 | ); 261 | path = Scenes; 262 | sourceTree = ""; 263 | }; 264 | C3DC24892A21FC9000341D68 /* Extensions */ = { 265 | isa = PBXGroup; 266 | children = ( 267 | C3DC24922A21FD0100341D68 /* View+Ext.swift */, 268 | C3DC24942A21FD2400341D68 /* Publisher+Ext.swift */, 269 | C3DC24962A21FD4000341D68 /* Binding+Ext.swift */, 270 | C3DC24A02A21FEB200341D68 /* ProgressHUD+Ext.swift */, 271 | ); 272 | path = Extensions; 273 | sourceTree = ""; 274 | }; 275 | C3DC248A2A21FCB600341D68 /* SearchRepo */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | C3DC248B2A21FCD600341D68 /* SearchRepoCoordinator.swift */, 279 | C3DC248C2A21FCD600341D68 /* SearchRepoView.swift */, 280 | C3DC248D2A21FCD600341D68 /* SearchRepoViewModel.swift */, 281 | C3DC24B62A22079100341D68 /* RepoRow.swift */, 282 | ); 283 | path = SearchRepo; 284 | sourceTree = ""; 285 | }; 286 | C3DC24912A21FCE600341D68 /* Assets */ = { 287 | isa = PBXGroup; 288 | children = ( 289 | C3DC24422A21FB0100341D68 /* Assets.xcassets */, 290 | ); 291 | path = Assets; 292 | sourceTree = ""; 293 | }; 294 | C3DC249D2A21FE6E00341D68 /* Common */ = { 295 | isa = PBXGroup; 296 | children = ( 297 | C3DC249E2A21FE7600341D68 /* Common.swift */, 298 | ); 299 | path = Common; 300 | sourceTree = ""; 301 | }; 302 | C3DC24A82A21FFD900341D68 /* Routers */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | C3DC24A92A21FFF100341D68 /* SearchRepoAPIRouter.swift */, 306 | ); 307 | path = Routers; 308 | sourceTree = ""; 309 | }; 310 | C3DC24AD2A22046D00341D68 /* DetailRepo */ = { 311 | isa = PBXGroup; 312 | children = ( 313 | C3DC24AF2A22048300341D68 /* DetailRepoView.swift */, 314 | C3DC24B02A22048300341D68 /* DetailRepoViewModel.swift */, 315 | ); 316 | path = DetailRepo; 317 | sourceTree = ""; 318 | }; 319 | /* End PBXGroup section */ 320 | 321 | /* Begin PBXNativeTarget section */ 322 | C3DC243A2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI */ = { 323 | isa = PBXNativeTarget; 324 | buildConfigurationList = C3DC245F2A21FB0100341D68 /* Build configuration list for PBXNativeTarget "CleanArchitecture-MVVM-C-SwiftUI" */; 325 | buildPhases = ( 326 | C3DC24372A21FB0000341D68 /* Sources */, 327 | C3DC24382A21FB0000341D68 /* Frameworks */, 328 | C3DC24392A21FB0000341D68 /* Resources */, 329 | ); 330 | buildRules = ( 331 | ); 332 | dependencies = ( 333 | ); 334 | name = "CleanArchitecture-MVVM-C-SwiftUI"; 335 | packageProductDependencies = ( 336 | C3DC24692A21FB3700341D68 /* Stinsen */, 337 | C3DC246F2A21FB4B00341D68 /* SwiftMessages */, 338 | C3DC24722A21FB5500341D68 /* ObjectMapper */, 339 | C3DC24752A21FB5D00341D68 /* Alamofire */, 340 | C3DC24782A21FB7900341D68 /* Factory */, 341 | C3DC24992A21FD6100341D68 /* ProgressHUD */, 342 | ); 343 | productName = "CleanArchitecture-MVVM-C-SwiftUI"; 344 | productReference = C3DC243B2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI.app */; 345 | productType = "com.apple.product-type.application"; 346 | }; 347 | C3DC244A2A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUITests */ = { 348 | isa = PBXNativeTarget; 349 | buildConfigurationList = C3DC24622A21FB0100341D68 /* Build configuration list for PBXNativeTarget "CleanArchitecture-MVVM-C-SwiftUITests" */; 350 | buildPhases = ( 351 | C3DC24472A21FB0100341D68 /* Sources */, 352 | C3DC24482A21FB0100341D68 /* Frameworks */, 353 | C3DC24492A21FB0100341D68 /* Resources */, 354 | ); 355 | buildRules = ( 356 | ); 357 | dependencies = ( 358 | C3DC244D2A21FB0100341D68 /* PBXTargetDependency */, 359 | ); 360 | name = "CleanArchitecture-MVVM-C-SwiftUITests"; 361 | productName = "CleanArchitecture-MVVM-C-SwiftUITests"; 362 | productReference = C3DC244B2A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUITests.xctest */; 363 | productType = "com.apple.product-type.bundle.unit-test"; 364 | }; 365 | C3DC24542A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUIUITests */ = { 366 | isa = PBXNativeTarget; 367 | buildConfigurationList = C3DC24652A21FB0100341D68 /* Build configuration list for PBXNativeTarget "CleanArchitecture-MVVM-C-SwiftUIUITests" */; 368 | buildPhases = ( 369 | C3DC24512A21FB0100341D68 /* Sources */, 370 | C3DC24522A21FB0100341D68 /* Frameworks */, 371 | C3DC24532A21FB0100341D68 /* Resources */, 372 | ); 373 | buildRules = ( 374 | ); 375 | dependencies = ( 376 | C3DC24572A21FB0100341D68 /* PBXTargetDependency */, 377 | ); 378 | name = "CleanArchitecture-MVVM-C-SwiftUIUITests"; 379 | productName = "CleanArchitecture-MVVM-C-SwiftUIUITests"; 380 | productReference = C3DC24552A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUIUITests.xctest */; 381 | productType = "com.apple.product-type.bundle.ui-testing"; 382 | }; 383 | /* End PBXNativeTarget section */ 384 | 385 | /* Begin PBXProject section */ 386 | C3DC24332A21FB0000341D68 /* Project object */ = { 387 | isa = PBXProject; 388 | attributes = { 389 | BuildIndependentTargetsInParallel = 1; 390 | LastSwiftUpdateCheck = 1430; 391 | LastUpgradeCheck = 1430; 392 | TargetAttributes = { 393 | C3DC243A2A21FB0000341D68 = { 394 | CreatedOnToolsVersion = 14.3; 395 | }; 396 | C3DC244A2A21FB0100341D68 = { 397 | CreatedOnToolsVersion = 14.3; 398 | TestTargetID = C3DC243A2A21FB0000341D68; 399 | }; 400 | C3DC24542A21FB0100341D68 = { 401 | CreatedOnToolsVersion = 14.3; 402 | TestTargetID = C3DC243A2A21FB0000341D68; 403 | }; 404 | }; 405 | }; 406 | buildConfigurationList = C3DC24362A21FB0000341D68 /* Build configuration list for PBXProject "CleanArchitecture-MVVM-C-SwiftUI" */; 407 | compatibilityVersion = "Xcode 14.0"; 408 | developmentRegion = en; 409 | hasScannedForEncodings = 0; 410 | knownRegions = ( 411 | en, 412 | Base, 413 | ); 414 | mainGroup = C3DC24322A21FB0000341D68; 415 | packageReferences = ( 416 | C3DC24682A21FB3700341D68 /* XCRemoteSwiftPackageReference "stinsen" */, 417 | C3DC246E2A21FB4B00341D68 /* XCRemoteSwiftPackageReference "SwiftMessages" */, 418 | C3DC24712A21FB5500341D68 /* XCRemoteSwiftPackageReference "ObjectMapper" */, 419 | C3DC24742A21FB5D00341D68 /* XCRemoteSwiftPackageReference "Alamofire" */, 420 | C3DC24772A21FB7900341D68 /* XCRemoteSwiftPackageReference "Factory" */, 421 | C3DC24982A21FD6100341D68 /* XCRemoteSwiftPackageReference "ProgressHUD" */, 422 | ); 423 | productRefGroup = C3DC243C2A21FB0000341D68 /* Products */; 424 | projectDirPath = ""; 425 | projectRoot = ""; 426 | targets = ( 427 | C3DC243A2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI */, 428 | C3DC244A2A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUITests */, 429 | C3DC24542A21FB0100341D68 /* CleanArchitecture-MVVM-C-SwiftUIUITests */, 430 | ); 431 | }; 432 | /* End PBXProject section */ 433 | 434 | /* Begin PBXResourcesBuildPhase section */ 435 | C3DC24392A21FB0000341D68 /* Resources */ = { 436 | isa = PBXResourcesBuildPhase; 437 | buildActionMask = 2147483647; 438 | files = ( 439 | C3DC24462A21FB0100341D68 /* Preview Assets.xcassets in Resources */, 440 | C3DC24432A21FB0100341D68 /* Assets.xcassets in Resources */, 441 | ); 442 | runOnlyForDeploymentPostprocessing = 0; 443 | }; 444 | C3DC24492A21FB0100341D68 /* Resources */ = { 445 | isa = PBXResourcesBuildPhase; 446 | buildActionMask = 2147483647; 447 | files = ( 448 | ); 449 | runOnlyForDeploymentPostprocessing = 0; 450 | }; 451 | C3DC24532A21FB0100341D68 /* Resources */ = { 452 | isa = PBXResourcesBuildPhase; 453 | buildActionMask = 2147483647; 454 | files = ( 455 | ); 456 | runOnlyForDeploymentPostprocessing = 0; 457 | }; 458 | /* End PBXResourcesBuildPhase section */ 459 | 460 | /* Begin PBXSourcesBuildPhase section */ 461 | C3DC24372A21FB0000341D68 /* Sources */ = { 462 | isa = PBXSourcesBuildPhase; 463 | buildActionMask = 2147483647; 464 | files = ( 465 | C3DC24A72A21FF1500341D68 /* APIService.swift in Sources */, 466 | C3DC24AC2A22011900341D68 /* AppDelegate+Injected.swift in Sources */, 467 | C3DC24862A21FC7900341D68 /* GithubRepoUseCase.swift in Sources */, 468 | C3DC249C2A21FDB500341D68 /* MainCoordinator.swift in Sources */, 469 | C3DC24A12A21FEB200341D68 /* ProgressHUD+Ext.swift in Sources */, 470 | C3DC249F2A21FE7600341D68 /* Common.swift in Sources */, 471 | C3DC24A32A21FEF400341D68 /* APIInputBase.swift in Sources */, 472 | C3DC24B32A22048300341D68 /* DetailRepoViewModel.swift in Sources */, 473 | C3DC24932A21FD0100341D68 /* View+Ext.swift in Sources */, 474 | C3DC248E2A21FCD600341D68 /* SearchRepoCoordinator.swift in Sources */, 475 | C3DC24952A21FD2400341D68 /* Publisher+Ext.swift in Sources */, 476 | C3DC248F2A21FCD600341D68 /* SearchRepoView.swift in Sources */, 477 | C3DC24902A21FCD600341D68 /* SearchRepoViewModel.swift in Sources */, 478 | C3DC24842A21FC2C00341D68 /* GithubRepoModel.swift in Sources */, 479 | C3DC243F2A21FB0000341D68 /* CleanArchitecture_MVVM_C_SwiftUIApp.swift in Sources */, 480 | C3DC24A52A21FEFD00341D68 /* APIError.swift in Sources */, 481 | C3DC24AA2A21FFF100341D68 /* SearchRepoAPIRouter.swift in Sources */, 482 | C3DC24B72A22079100341D68 /* RepoRow.swift in Sources */, 483 | C3DC24B22A22048300341D68 /* DetailRepoView.swift in Sources */, 484 | C3DC24882A21FC8600341D68 /* GithubRepoRepository.swift in Sources */, 485 | C3DC24972A21FD4000341D68 /* Binding+Ext.swift in Sources */, 486 | ); 487 | runOnlyForDeploymentPostprocessing = 0; 488 | }; 489 | C3DC24472A21FB0100341D68 /* Sources */ = { 490 | isa = PBXSourcesBuildPhase; 491 | buildActionMask = 2147483647; 492 | files = ( 493 | C3DC24502A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUITests.swift in Sources */, 494 | ); 495 | runOnlyForDeploymentPostprocessing = 0; 496 | }; 497 | C3DC24512A21FB0100341D68 /* Sources */ = { 498 | isa = PBXSourcesBuildPhase; 499 | buildActionMask = 2147483647; 500 | files = ( 501 | C3DC245A2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITests.swift in Sources */, 502 | C3DC245C2A21FB0100341D68 /* CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift in Sources */, 503 | ); 504 | runOnlyForDeploymentPostprocessing = 0; 505 | }; 506 | /* End PBXSourcesBuildPhase section */ 507 | 508 | /* Begin PBXTargetDependency section */ 509 | C3DC244D2A21FB0100341D68 /* PBXTargetDependency */ = { 510 | isa = PBXTargetDependency; 511 | target = C3DC243A2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI */; 512 | targetProxy = C3DC244C2A21FB0100341D68 /* PBXContainerItemProxy */; 513 | }; 514 | C3DC24572A21FB0100341D68 /* PBXTargetDependency */ = { 515 | isa = PBXTargetDependency; 516 | target = C3DC243A2A21FB0000341D68 /* CleanArchitecture-MVVM-C-SwiftUI */; 517 | targetProxy = C3DC24562A21FB0100341D68 /* PBXContainerItemProxy */; 518 | }; 519 | /* End PBXTargetDependency section */ 520 | 521 | /* Begin XCBuildConfiguration section */ 522 | C3DC245D2A21FB0100341D68 /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | ALWAYS_SEARCH_USER_PATHS = NO; 526 | CLANG_ANALYZER_NONNULL = YES; 527 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 528 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 529 | CLANG_ENABLE_MODULES = YES; 530 | CLANG_ENABLE_OBJC_ARC = YES; 531 | CLANG_ENABLE_OBJC_WEAK = YES; 532 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 533 | CLANG_WARN_BOOL_CONVERSION = YES; 534 | CLANG_WARN_COMMA = YES; 535 | CLANG_WARN_CONSTANT_CONVERSION = YES; 536 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 537 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 538 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 539 | CLANG_WARN_EMPTY_BODY = YES; 540 | CLANG_WARN_ENUM_CONVERSION = YES; 541 | CLANG_WARN_INFINITE_RECURSION = YES; 542 | CLANG_WARN_INT_CONVERSION = YES; 543 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 544 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 545 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 546 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 547 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 548 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 549 | CLANG_WARN_STRICT_PROTOTYPES = YES; 550 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 551 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 552 | CLANG_WARN_UNREACHABLE_CODE = YES; 553 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 554 | COPY_PHASE_STRIP = NO; 555 | DEBUG_INFORMATION_FORMAT = dwarf; 556 | ENABLE_STRICT_OBJC_MSGSEND = YES; 557 | ENABLE_TESTABILITY = YES; 558 | GCC_C_LANGUAGE_STANDARD = gnu11; 559 | GCC_DYNAMIC_NO_PIC = NO; 560 | GCC_NO_COMMON_BLOCKS = YES; 561 | GCC_OPTIMIZATION_LEVEL = 0; 562 | GCC_PREPROCESSOR_DEFINITIONS = ( 563 | "DEBUG=1", 564 | "$(inherited)", 565 | ); 566 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 567 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 568 | GCC_WARN_UNDECLARED_SELECTOR = YES; 569 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 570 | GCC_WARN_UNUSED_FUNCTION = YES; 571 | GCC_WARN_UNUSED_VARIABLE = YES; 572 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 573 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 574 | MTL_FAST_MATH = YES; 575 | ONLY_ACTIVE_ARCH = YES; 576 | SDKROOT = iphoneos; 577 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 578 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 579 | }; 580 | name = Debug; 581 | }; 582 | C3DC245E2A21FB0100341D68 /* Release */ = { 583 | isa = XCBuildConfiguration; 584 | buildSettings = { 585 | ALWAYS_SEARCH_USER_PATHS = NO; 586 | CLANG_ANALYZER_NONNULL = YES; 587 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 588 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 589 | CLANG_ENABLE_MODULES = YES; 590 | CLANG_ENABLE_OBJC_ARC = YES; 591 | CLANG_ENABLE_OBJC_WEAK = YES; 592 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 593 | CLANG_WARN_BOOL_CONVERSION = YES; 594 | CLANG_WARN_COMMA = YES; 595 | CLANG_WARN_CONSTANT_CONVERSION = YES; 596 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 597 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 598 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 599 | CLANG_WARN_EMPTY_BODY = YES; 600 | CLANG_WARN_ENUM_CONVERSION = YES; 601 | CLANG_WARN_INFINITE_RECURSION = YES; 602 | CLANG_WARN_INT_CONVERSION = YES; 603 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 604 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 605 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 606 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 607 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 608 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 609 | CLANG_WARN_STRICT_PROTOTYPES = YES; 610 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 611 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 612 | CLANG_WARN_UNREACHABLE_CODE = YES; 613 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 614 | COPY_PHASE_STRIP = NO; 615 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 616 | ENABLE_NS_ASSERTIONS = NO; 617 | ENABLE_STRICT_OBJC_MSGSEND = YES; 618 | GCC_C_LANGUAGE_STANDARD = gnu11; 619 | GCC_NO_COMMON_BLOCKS = YES; 620 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 621 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 622 | GCC_WARN_UNDECLARED_SELECTOR = YES; 623 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 624 | GCC_WARN_UNUSED_FUNCTION = YES; 625 | GCC_WARN_UNUSED_VARIABLE = YES; 626 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 627 | MTL_ENABLE_DEBUG_INFO = NO; 628 | MTL_FAST_MATH = YES; 629 | SDKROOT = iphoneos; 630 | SWIFT_COMPILATION_MODE = wholemodule; 631 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 632 | VALIDATE_PRODUCT = YES; 633 | }; 634 | name = Release; 635 | }; 636 | C3DC24602A21FB0100341D68 /* Debug */ = { 637 | isa = XCBuildConfiguration; 638 | buildSettings = { 639 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 640 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 641 | CODE_SIGN_STYLE = Automatic; 642 | CURRENT_PROJECT_VERSION = 1; 643 | DEVELOPMENT_ASSET_PATHS = "\"CleanArchitecture-MVVM-C-SwiftUI/Preview Content\""; 644 | DEVELOPMENT_TEAM = KWZTNUQM3X; 645 | ENABLE_PREVIEWS = YES; 646 | GENERATE_INFOPLIST_FILE = YES; 647 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 648 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 649 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 650 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 651 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 652 | LD_RUNPATH_SEARCH_PATHS = ( 653 | "$(inherited)", 654 | "@executable_path/Frameworks", 655 | ); 656 | MARKETING_VERSION = 1.0; 657 | PRODUCT_BUNDLE_IDENTIFIER = "com.huyparody.CleanArchitecture-MVVM-C-SwiftUI"; 658 | PRODUCT_NAME = "$(TARGET_NAME)"; 659 | SWIFT_EMIT_LOC_STRINGS = YES; 660 | SWIFT_VERSION = 5.0; 661 | TARGETED_DEVICE_FAMILY = "1,2"; 662 | }; 663 | name = Debug; 664 | }; 665 | C3DC24612A21FB0100341D68 /* Release */ = { 666 | isa = XCBuildConfiguration; 667 | buildSettings = { 668 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 669 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 670 | CODE_SIGN_STYLE = Automatic; 671 | CURRENT_PROJECT_VERSION = 1; 672 | DEVELOPMENT_ASSET_PATHS = "\"CleanArchitecture-MVVM-C-SwiftUI/Preview Content\""; 673 | DEVELOPMENT_TEAM = KWZTNUQM3X; 674 | ENABLE_PREVIEWS = YES; 675 | GENERATE_INFOPLIST_FILE = YES; 676 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 677 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 678 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 679 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 680 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 681 | LD_RUNPATH_SEARCH_PATHS = ( 682 | "$(inherited)", 683 | "@executable_path/Frameworks", 684 | ); 685 | MARKETING_VERSION = 1.0; 686 | PRODUCT_BUNDLE_IDENTIFIER = "com.huyparody.CleanArchitecture-MVVM-C-SwiftUI"; 687 | PRODUCT_NAME = "$(TARGET_NAME)"; 688 | SWIFT_EMIT_LOC_STRINGS = YES; 689 | SWIFT_VERSION = 5.0; 690 | TARGETED_DEVICE_FAMILY = "1,2"; 691 | }; 692 | name = Release; 693 | }; 694 | C3DC24632A21FB0100341D68 /* Debug */ = { 695 | isa = XCBuildConfiguration; 696 | buildSettings = { 697 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 698 | BUNDLE_LOADER = "$(TEST_HOST)"; 699 | CODE_SIGN_STYLE = Automatic; 700 | CURRENT_PROJECT_VERSION = 1; 701 | DEVELOPMENT_TEAM = KWZTNUQM3X; 702 | GENERATE_INFOPLIST_FILE = YES; 703 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 704 | MARKETING_VERSION = 1.0; 705 | PRODUCT_BUNDLE_IDENTIFIER = "com.huyparody.CleanArchitecture-MVVM-C-SwiftUITests"; 706 | PRODUCT_NAME = "$(TARGET_NAME)"; 707 | SWIFT_EMIT_LOC_STRINGS = NO; 708 | SWIFT_VERSION = 5.0; 709 | TARGETED_DEVICE_FAMILY = "1,2"; 710 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitecture-MVVM-C-SwiftUI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CleanArchitecture-MVVM-C-SwiftUI"; 711 | }; 712 | name = Debug; 713 | }; 714 | C3DC24642A21FB0100341D68 /* Release */ = { 715 | isa = XCBuildConfiguration; 716 | buildSettings = { 717 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 718 | BUNDLE_LOADER = "$(TEST_HOST)"; 719 | CODE_SIGN_STYLE = Automatic; 720 | CURRENT_PROJECT_VERSION = 1; 721 | DEVELOPMENT_TEAM = KWZTNUQM3X; 722 | GENERATE_INFOPLIST_FILE = YES; 723 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 724 | MARKETING_VERSION = 1.0; 725 | PRODUCT_BUNDLE_IDENTIFIER = "com.huyparody.CleanArchitecture-MVVM-C-SwiftUITests"; 726 | PRODUCT_NAME = "$(TARGET_NAME)"; 727 | SWIFT_EMIT_LOC_STRINGS = NO; 728 | SWIFT_VERSION = 5.0; 729 | TARGETED_DEVICE_FAMILY = "1,2"; 730 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CleanArchitecture-MVVM-C-SwiftUI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CleanArchitecture-MVVM-C-SwiftUI"; 731 | }; 732 | name = Release; 733 | }; 734 | C3DC24662A21FB0100341D68 /* Debug */ = { 735 | isa = XCBuildConfiguration; 736 | buildSettings = { 737 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 738 | CODE_SIGN_STYLE = Automatic; 739 | CURRENT_PROJECT_VERSION = 1; 740 | DEVELOPMENT_TEAM = KWZTNUQM3X; 741 | GENERATE_INFOPLIST_FILE = YES; 742 | MARKETING_VERSION = 1.0; 743 | PRODUCT_BUNDLE_IDENTIFIER = "com.huyparody.CleanArchitecture-MVVM-C-SwiftUIUITests"; 744 | PRODUCT_NAME = "$(TARGET_NAME)"; 745 | SWIFT_EMIT_LOC_STRINGS = NO; 746 | SWIFT_VERSION = 5.0; 747 | TARGETED_DEVICE_FAMILY = "1,2"; 748 | TEST_TARGET_NAME = "CleanArchitecture-MVVM-C-SwiftUI"; 749 | }; 750 | name = Debug; 751 | }; 752 | C3DC24672A21FB0100341D68 /* Release */ = { 753 | isa = XCBuildConfiguration; 754 | buildSettings = { 755 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 756 | CODE_SIGN_STYLE = Automatic; 757 | CURRENT_PROJECT_VERSION = 1; 758 | DEVELOPMENT_TEAM = KWZTNUQM3X; 759 | GENERATE_INFOPLIST_FILE = YES; 760 | MARKETING_VERSION = 1.0; 761 | PRODUCT_BUNDLE_IDENTIFIER = "com.huyparody.CleanArchitecture-MVVM-C-SwiftUIUITests"; 762 | PRODUCT_NAME = "$(TARGET_NAME)"; 763 | SWIFT_EMIT_LOC_STRINGS = NO; 764 | SWIFT_VERSION = 5.0; 765 | TARGETED_DEVICE_FAMILY = "1,2"; 766 | TEST_TARGET_NAME = "CleanArchitecture-MVVM-C-SwiftUI"; 767 | }; 768 | name = Release; 769 | }; 770 | /* End XCBuildConfiguration section */ 771 | 772 | /* Begin XCConfigurationList section */ 773 | C3DC24362A21FB0000341D68 /* Build configuration list for PBXProject "CleanArchitecture-MVVM-C-SwiftUI" */ = { 774 | isa = XCConfigurationList; 775 | buildConfigurations = ( 776 | C3DC245D2A21FB0100341D68 /* Debug */, 777 | C3DC245E2A21FB0100341D68 /* Release */, 778 | ); 779 | defaultConfigurationIsVisible = 0; 780 | defaultConfigurationName = Release; 781 | }; 782 | C3DC245F2A21FB0100341D68 /* Build configuration list for PBXNativeTarget "CleanArchitecture-MVVM-C-SwiftUI" */ = { 783 | isa = XCConfigurationList; 784 | buildConfigurations = ( 785 | C3DC24602A21FB0100341D68 /* Debug */, 786 | C3DC24612A21FB0100341D68 /* Release */, 787 | ); 788 | defaultConfigurationIsVisible = 0; 789 | defaultConfigurationName = Release; 790 | }; 791 | C3DC24622A21FB0100341D68 /* Build configuration list for PBXNativeTarget "CleanArchitecture-MVVM-C-SwiftUITests" */ = { 792 | isa = XCConfigurationList; 793 | buildConfigurations = ( 794 | C3DC24632A21FB0100341D68 /* Debug */, 795 | C3DC24642A21FB0100341D68 /* Release */, 796 | ); 797 | defaultConfigurationIsVisible = 0; 798 | defaultConfigurationName = Release; 799 | }; 800 | C3DC24652A21FB0100341D68 /* Build configuration list for PBXNativeTarget "CleanArchitecture-MVVM-C-SwiftUIUITests" */ = { 801 | isa = XCConfigurationList; 802 | buildConfigurations = ( 803 | C3DC24662A21FB0100341D68 /* Debug */, 804 | C3DC24672A21FB0100341D68 /* Release */, 805 | ); 806 | defaultConfigurationIsVisible = 0; 807 | defaultConfigurationName = Release; 808 | }; 809 | /* End XCConfigurationList section */ 810 | 811 | /* Begin XCRemoteSwiftPackageReference section */ 812 | C3DC24682A21FB3700341D68 /* XCRemoteSwiftPackageReference "stinsen" */ = { 813 | isa = XCRemoteSwiftPackageReference; 814 | repositoryURL = "https://github.com/rundfunk47/stinsen.git"; 815 | requirement = { 816 | kind = upToNextMajorVersion; 817 | minimumVersion = 2.0.0; 818 | }; 819 | }; 820 | C3DC246E2A21FB4B00341D68 /* XCRemoteSwiftPackageReference "SwiftMessages" */ = { 821 | isa = XCRemoteSwiftPackageReference; 822 | repositoryURL = "https://github.com/SwiftKickMobile/SwiftMessages.git"; 823 | requirement = { 824 | kind = upToNextMajorVersion; 825 | minimumVersion = 9.0.0; 826 | }; 827 | }; 828 | C3DC24712A21FB5500341D68 /* XCRemoteSwiftPackageReference "ObjectMapper" */ = { 829 | isa = XCRemoteSwiftPackageReference; 830 | repositoryURL = "https://github.com/tristanhimmelman/ObjectMapper.git"; 831 | requirement = { 832 | kind = upToNextMajorVersion; 833 | minimumVersion = 4.0.0; 834 | }; 835 | }; 836 | C3DC24742A21FB5D00341D68 /* XCRemoteSwiftPackageReference "Alamofire" */ = { 837 | isa = XCRemoteSwiftPackageReference; 838 | repositoryURL = "https://github.com/Alamofire/Alamofire.git"; 839 | requirement = { 840 | kind = upToNextMajorVersion; 841 | minimumVersion = 5.0.0; 842 | }; 843 | }; 844 | C3DC24772A21FB7900341D68 /* XCRemoteSwiftPackageReference "Factory" */ = { 845 | isa = XCRemoteSwiftPackageReference; 846 | repositoryURL = "https://github.com/hmlongco/Factory.git"; 847 | requirement = { 848 | kind = upToNextMajorVersion; 849 | minimumVersion = 2.0.0; 850 | }; 851 | }; 852 | C3DC24982A21FD6100341D68 /* XCRemoteSwiftPackageReference "ProgressHUD" */ = { 853 | isa = XCRemoteSwiftPackageReference; 854 | repositoryURL = "https://github.com/relatedcode/ProgressHUD.git"; 855 | requirement = { 856 | kind = upToNextMajorVersion; 857 | minimumVersion = 13.0.0; 858 | }; 859 | }; 860 | /* End XCRemoteSwiftPackageReference section */ 861 | 862 | /* Begin XCSwiftPackageProductDependency section */ 863 | C3DC24692A21FB3700341D68 /* Stinsen */ = { 864 | isa = XCSwiftPackageProductDependency; 865 | package = C3DC24682A21FB3700341D68 /* XCRemoteSwiftPackageReference "stinsen" */; 866 | productName = Stinsen; 867 | }; 868 | C3DC246F2A21FB4B00341D68 /* SwiftMessages */ = { 869 | isa = XCSwiftPackageProductDependency; 870 | package = C3DC246E2A21FB4B00341D68 /* XCRemoteSwiftPackageReference "SwiftMessages" */; 871 | productName = SwiftMessages; 872 | }; 873 | C3DC24722A21FB5500341D68 /* ObjectMapper */ = { 874 | isa = XCSwiftPackageProductDependency; 875 | package = C3DC24712A21FB5500341D68 /* XCRemoteSwiftPackageReference "ObjectMapper" */; 876 | productName = ObjectMapper; 877 | }; 878 | C3DC24752A21FB5D00341D68 /* Alamofire */ = { 879 | isa = XCSwiftPackageProductDependency; 880 | package = C3DC24742A21FB5D00341D68 /* XCRemoteSwiftPackageReference "Alamofire" */; 881 | productName = Alamofire; 882 | }; 883 | C3DC24782A21FB7900341D68 /* Factory */ = { 884 | isa = XCSwiftPackageProductDependency; 885 | package = C3DC24772A21FB7900341D68 /* XCRemoteSwiftPackageReference "Factory" */; 886 | productName = Factory; 887 | }; 888 | C3DC24992A21FD6100341D68 /* ProgressHUD */ = { 889 | isa = XCSwiftPackageProductDependency; 890 | package = C3DC24982A21FD6100341D68 /* XCRemoteSwiftPackageReference "ProgressHUD" */; 891 | productName = ProgressHUD; 892 | }; 893 | /* End XCSwiftPackageProductDependency section */ 894 | }; 895 | rootObject = C3DC24332A21FB0000341D68 /* Project object */; 896 | } 897 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/App/AppDelegate+Injected.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+Injected.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Factory 10 | 11 | extension Container { 12 | 13 | var githubRepository: Factory { 14 | self { 15 | GithubRepoRepository() 16 | } 17 | } 18 | 19 | var githubUseCase: Factory { 20 | self { 21 | GithubRepoUseCase() 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/App/CleanArchitecture_MVVM_C_SwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CleanArchitecture_MVVM_C_SwiftUIApp.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct CleanArchitecture_MVVM_C_SwiftUIApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | MainCoordinator().view() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/App/MainCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainCoordinator.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Stinsen 10 | import SwiftUI 11 | 12 | final class MainCoordinator: NavigationCoordinatable { 13 | 14 | var stack: Stinsen.NavigationStack 15 | 16 | @Root var searchRepo = makeSearchRepo 17 | 18 | init() { 19 | stack = NavigationStack(initial: \MainCoordinator.searchRepo) 20 | } 21 | 22 | @ViewBuilder func sharedView(_ view: AnyView) -> some View { 23 | view 24 | .onAppear { 25 | self.root(\.searchRepo) 26 | } 27 | } 28 | 29 | func customize(_ view: AnyView) -> some View { 30 | sharedView(view) 31 | } 32 | 33 | } 34 | 35 | extension MainCoordinator { 36 | 37 | func makeSearchRepo() -> NavigationViewCoordinator { 38 | NavigationViewCoordinator(SearchRepoCoordinator()) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Common/Common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Common.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftMessages 10 | 11 | class Common { 12 | 13 | static func showError(_ error: Error?) { 14 | if error != nil { 15 | if let error = error as? APIError { 16 | showSMErrorAlert(error.displayText) 17 | } 18 | else { 19 | let errorCode = (error as? NSError)?.code 20 | ///Error code = 13: NO interet connection 21 | 22 | if errorCode == 13 { 23 | showSMErrorAlert("There is no internet connection", type: .warning) 24 | } 25 | else { 26 | showSMErrorAlert(error?.localizedDescription ?? "") 27 | } 28 | } 29 | } 30 | } 31 | 32 | static func showSMErrorAlert(_ message: String, type: Theme = .error) { 33 | SwiftMessages.show { 34 | let view = MessageView.viewFromNib(layout: .tabView) 35 | view.configureTheme(type) 36 | view.configureContent(title: "", body: message) 37 | view.configureDropShadow() 38 | view.button?.isHidden = true 39 | return view 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Domain/Entities/GithubRepoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepoModel.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import UIKit 9 | import ObjectMapper 10 | 11 | struct GithubRepoModel: Mappable { 12 | 13 | var githubRepos: [GithubRepoEntities]? 14 | 15 | init?(map: Map) { 16 | 17 | } 18 | 19 | mutating func mapping(map: Map) { 20 | githubRepos <- map["items"] 21 | } 22 | } 23 | 24 | typealias Model = Mappable & Identifiable 25 | 26 | struct GithubRepoEntities: Model { 27 | 28 | var id: Int? 29 | var name: String? 30 | var fullname: String? 31 | 32 | init?(map: Map) { 33 | 34 | } 35 | 36 | mutating func mapping(map: Map) { 37 | id <- map["id"] 38 | name <- map["name"] 39 | fullname <- map["full_name"] 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Domain/UseCases/GithubRepoUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepoUseCase.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Factory 10 | import Combine 11 | 12 | protocol GithubRepoUseCaseType { 13 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> 14 | } 15 | 16 | class GithubRepoUseCase: GithubRepoUseCaseType { 17 | 18 | @LazyInjected(\.githubRepository) var repository 19 | 20 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> { 21 | repository.searchRepo(query: query) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Extensions/Binding+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding+Ext.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Binding { 12 | public func defaultValue(_ value: T) -> Binding where Value == Optional { 13 | Binding { 14 | wrappedValue ?? value 15 | } set: { 16 | wrappedValue = $0 17 | } 18 | } 19 | 20 | } 21 | 22 | extension Binding where Value == Optional { 23 | public var orEmpty: Binding { 24 | Binding { 25 | wrappedValue ?? "" 26 | } set: { 27 | wrappedValue = $0 28 | } 29 | } 30 | } 31 | 32 | extension Binding where Value == Optional { 33 | public var orEmpty: Binding { 34 | Binding { 35 | wrappedValue ?? false 36 | } set: { 37 | wrappedValue = $0 38 | } 39 | } 40 | } 41 | 42 | extension Binding where Value == Optional { 43 | public var orEmpty: Binding { 44 | Binding { 45 | wrappedValue ?? 0 46 | } set: { 47 | wrappedValue = $0 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Extensions/ProgressHUD+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressHUD+Ext.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import ProgressHUD 10 | 11 | extension ProgressHUD { 12 | 13 | static func commonLoading(_ willLoading: Bool) { 14 | if willLoading { 15 | self.colorBackground = .black.withAlphaComponent(0.25) 16 | self.show(interaction: false) 17 | } 18 | else { 19 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 20 | self.dismiss() 21 | } 22 | } 23 | 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Extensions/Publisher+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Ext.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class ErrorTracker { 12 | private let subject = PassthroughSubject() 13 | 14 | var errorPublisher: AnyPublisher { 15 | subject.eraseToAnyPublisher() 16 | } 17 | 18 | func track(_ error: Error) { 19 | subject.send(error) 20 | } 21 | } 22 | 23 | extension Publisher { 24 | func trackError(_ tracker: Tracker) -> AnyPublisher where Self.Failure == Error { 25 | self 26 | .catch { error -> Empty in 27 | tracker.track(error) 28 | return Empty() 29 | } 30 | .eraseToAnyPublisher() 31 | } 32 | } 33 | 34 | extension Publisher { 35 | func trackActivity(_ activityIndicator: ActivityIndicator) -> AnyPublisher { 36 | activityIndicator.trackActivity(self) 37 | } 38 | } 39 | 40 | class ActivityIndicator { 41 | @Published private var count = 0 42 | private let lock = NSRecursiveLock() 43 | 44 | var isLoadingPublisher: AnyPublisher { 45 | $count 46 | .map({$0 > 0}) 47 | .eraseToAnyPublisher() 48 | 49 | } 50 | 51 | func trackActivity(_ source: T) -> AnyPublisher { 52 | return source 53 | .handleEvents(receiveCompletion: { _ in 54 | self.decrement() 55 | }, receiveCancel: { 56 | self.decrement() 57 | }, receiveRequest: { [weak self] _ in 58 | self?.increment() 59 | }) 60 | .eraseToAnyPublisher() 61 | } 62 | 63 | private func increment() { 64 | lock.lock() 65 | defer { lock.unlock() } 66 | 67 | count += 1 68 | } 69 | 70 | private func decrement() { 71 | lock.lock() 72 | defer { lock.unlock() } 73 | 74 | count -= 1 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Extensions/View+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Ext.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Combine 11 | import ProgressHUD 12 | 13 | extension View { 14 | func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { 15 | clipShape( RoundedCorner(radius: radius, corners: corners) ) 16 | } 17 | 18 | func roundedBorder(cornerRadius: CGFloat, lineWidth: CGFloat, borderColor: Color) -> some View { 19 | overlay( /// apply a rounded border 20 | RoundedRectangle(cornerRadius: cornerRadius) 21 | .stroke(borderColor, lineWidth: lineWidth) 22 | ) 23 | } 24 | 25 | func hideListRowSeperator() -> some View { 26 | if #available(iOS 15, *) { 27 | return AnyView(self.listRowSeparator(.hidden)) 28 | } else { 29 | return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) 30 | .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1)) 31 | .background(Color(.systemBackground))) 32 | } 33 | } 34 | 35 | 36 | /// Reads the view frame and bind it to the reader. 37 | /// - Parameters: 38 | /// - coordinateSpace: a coordinate space for the geometry reader. 39 | /// - reader: a reader of the view frame. 40 | func readFrame(in coordinateSpace: CoordinateSpace = .global, 41 | for reader: Binding) -> some View { 42 | readFrame(in: coordinateSpace) { value in 43 | reader.wrappedValue = value 44 | } 45 | } 46 | 47 | /// Reads the view frame and send it to the reader. 48 | /// - Parameters: 49 | /// - coordinateSpace: a coordinate space for the geometry reader. 50 | /// - reader: a reader of the view frame. 51 | func readFrame(in coordinateSpace: CoordinateSpace = .global, 52 | for reader: @escaping (CGRect) -> Void) -> some View { 53 | background( 54 | GeometryReader { geometryProxy in 55 | Color.clear 56 | .preference( 57 | key: FramePreferenceKey.self, 58 | value: geometryProxy.frame(in: coordinateSpace) 59 | ) 60 | .onPreferenceChange(FramePreferenceKey.self, perform: reader) 61 | } 62 | ) 63 | } 64 | 65 | func eraseToAnyView() -> AnyView { 66 | AnyView(self) 67 | } 68 | 69 | func hideNavBar() -> some View { 70 | if #available(iOS 16.0, *) { 71 | return AnyView(self.toolbar(.hidden, for: .navigationBar)) 72 | } else { 73 | return AnyView(self.navigationBarHidden(true)) 74 | } 75 | } 76 | 77 | func hideViewWhenDataNotAvailable(dataSource: Any?) -> some View { 78 | opacity(dataSource == nil ? 0 : 1) 79 | } 80 | 81 | func onViewDidLoad(perform action: (() -> Void)? = nil) -> some View { 82 | self.modifier(ViewDidLoadModifier(action: action)) 83 | } 84 | 85 | func onReceiveError(_ publisher: AnyPublisher) -> some View { 86 | onReceive(publisher) { error in 87 | Common.showError(error) 88 | } 89 | } 90 | 91 | func onReceiveLoading(_ publisher: AnyPublisher) -> some View { 92 | onReceive(publisher) { isLoading in 93 | ProgressHUD.commonLoading(isLoading) 94 | } 95 | } 96 | 97 | func changeToBlackWhenNotNil(_ data: String?, colorHint: Color) -> some View { 98 | if data == nil || data?.isEmpty ?? false { 99 | return self.foregroundColor(colorHint) 100 | } 101 | else { 102 | return self.foregroundColor(.black) 103 | } 104 | } 105 | 106 | } 107 | 108 | struct ViewDidLoadModifier: ViewModifier { 109 | @State private var viewDidLoad = false 110 | let action: (() -> Void)? 111 | 112 | func body(content: Content) -> some View { 113 | content 114 | .onAppear { 115 | if !viewDidLoad { 116 | viewDidLoad = true 117 | action?() 118 | } 119 | } 120 | } 121 | } 122 | 123 | struct FramePreferenceKey: PreferenceKey { 124 | static var defaultValue = CGRect.zero 125 | 126 | static func reduce(value: inout CGRect, nextValue: () -> CGRect) { 127 | value = nextValue() 128 | } 129 | } 130 | 131 | struct RoundedCorner: Shape { 132 | 133 | var radius: CGFloat = .infinity 134 | var corners: UIRectCorner = .allCorners 135 | 136 | func path(in rect: CGRect) -> Path { 137 | let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) 138 | return Path(path.cgPath) 139 | } 140 | } 141 | 142 | extension UINavigationController { 143 | 144 | ///Re-enable swipe to go back when hide nav bar 145 | override open func viewDidLoad() { 146 | super.viewDidLoad() 147 | interactivePopGestureRecognizer?.delegate = nil 148 | } 149 | } 150 | 151 | public extension Binding where Value: Equatable { 152 | init(_ source: Binding, deselectTo value: Value) { 153 | self.init(get: { source.wrappedValue }, 154 | set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 } 155 | ) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Platform/Repositories/GithubRepoRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepoRepository.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | protocol GithubRepositoryType { 12 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> 13 | } 14 | 15 | class GithubRepoRepository: GithubRepositoryType { 16 | 17 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> { 18 | 19 | let param: [String: Any] = [ 20 | "q": query, 21 | "per_page": 10, 22 | "page": 1 23 | ] 24 | 25 | return APIService 26 | .shared 27 | .request(nonBaseResponse: SearchRepoAPIRouter.searchRepo(param: param)) 28 | .tryMap { (response: GithubRepoModel) in 29 | return response.githubRepos ?? [] 30 | } 31 | .eraseToAnyPublisher() 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Platform/Routers/SearchRepoAPIRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchRepoAPIRouter.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import UIKit 9 | import Alamofire 10 | 11 | enum SearchRepoAPIRouter { 12 | case searchRepo(param: [String: Any]) 13 | } 14 | 15 | extension SearchRepoAPIRouter: APIInputBase { 16 | 17 | var headers: HTTPHeaders { 18 | var header = HTTPHeaders() 19 | // if requireToken { 20 | // header.add(.authorization(bearerToken: AuthenticationService.shared.getToken() ?? "")) 21 | // } 22 | header.add(.accept("application/json")) 23 | return header 24 | } 25 | 26 | var url: URL { 27 | // return Config.baseURL.appendingPathComponent(path) 28 | let baseURL = URL.init(string: "https://api.github.com/search/repositories")! 29 | return baseURL 30 | } 31 | 32 | var method: HTTPMethod { 33 | switch self { 34 | case .searchRepo: 35 | return .get 36 | } 37 | } 38 | 39 | var encoding: ParameterEncoding { 40 | return method == .get ? URLEncoding.default : JSONEncoding.default 41 | } 42 | 43 | 44 | var parameters: [String : Any]? { 45 | switch self { 46 | case .searchRepo(let param): 47 | return param 48 | } 49 | } 50 | 51 | var path: String { 52 | switch self { 53 | case .searchRepo: 54 | return "" 55 | } 56 | } 57 | 58 | var requireToken: Bool { 59 | switch self { 60 | case .searchRepo: 61 | return false 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Platform/Services/APIError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIError.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum APIError: Error { 11 | case error(code: Int, message: String) 12 | case invalidResponseData(data: Any) 13 | case unknown 14 | 15 | public var displayText: String { 16 | switch self { 17 | case .invalidResponseData: 18 | return "Invalid response" 19 | case .error(_, let message): 20 | //switch errorResponseCode 21 | return message 22 | case .unknown: 23 | return "Unknown error" 24 | } 25 | } 26 | 27 | public var code: Int { 28 | switch self { 29 | case .error(let code, _): 30 | return code 31 | default : 32 | return 0 33 | } 34 | } 35 | } 36 | 37 | //enum StatusCode: String { 38 | // 39 | // case success = "HTTP_OK" 40 | // case emailUsed = "EMAIL_COMPANY_USED" 41 | // case unAuthorized = "HTTP_UNAUTHORIZED" 42 | // case invalidAccount = "INVALID_ACCOUNT" 43 | // case forbidden = "HTTP_FORBIDDEN" 44 | // case notFound = "HTTP_NOT_FOUND" 45 | // 46 | //} 47 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Platform/Services/APIInputBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIInputBase.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Alamofire 10 | 11 | protocol APIInputBase { 12 | var headers: HTTPHeaders { get } 13 | var url: URL { get } 14 | var method: HTTPMethod { get } 15 | var encoding: ParameterEncoding { get } 16 | var parameters: [String: Any]? { get } 17 | var requireToken: Bool { get } 18 | } 19 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Platform/Services/APIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIService.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Alamofire 10 | import ObjectMapper 11 | import Combine 12 | 13 | struct NetworkManager { 14 | 15 | static let kRequestTimeOut: TimeInterval = 30 16 | 17 | static let session: Session = { 18 | let configuration: URLSessionConfiguration = { 19 | let config = URLSessionConfiguration.default 20 | config.timeoutIntervalForRequest = kRequestTimeOut 21 | config.timeoutIntervalForResource = kRequestTimeOut 22 | config.httpMaximumConnectionsPerHost = 10 23 | return config 24 | }() 25 | let session = Session(configuration: configuration, serverTrustManager: nil) 26 | return session 27 | }() 28 | 29 | } 30 | 31 | class APIService { 32 | 33 | private let session = NetworkManager.session 34 | 35 | static let shared: APIService = { 36 | let instance = APIService() 37 | return instance 38 | }() 39 | 40 | func request(nonBaseResponse input: APIInputBase) -> Future { 41 | 42 | return Future { promise in 43 | self.session.request( 44 | input.url, 45 | method: input.method, 46 | parameters: input.parameters, 47 | encoding: input.encoding, 48 | headers: input.headers) 49 | .responseData(queue: .global(qos: .background)) { dataRequest in 50 | switch dataRequest.result { 51 | case .success(let value): 52 | do { 53 | let any = try JSONSerialization.jsonObject(with: value) 54 | if let dict = any as? [String: Any], let json = T.init(JSON: dict) { 55 | if dataRequest.response?.statusCode == 200 { 56 | promise(.success(json)) 57 | } 58 | else { 59 | promise(.failure(APIError.error(code: dataRequest.response?.statusCode ?? 0, message: "Something wrong, try again!"))) 60 | } 61 | } 62 | else { 63 | promise(.failure(APIError.invalidResponseData(data: value))) 64 | } 65 | } 66 | catch { 67 | promise(.failure(APIError.invalidResponseData(data: value))) 68 | } 69 | case .failure(let error): 70 | promise(.failure(error)) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Resources/Assets/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 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Resources/Assets/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 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Resources/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Scenes/DetailRepo/DetailRepoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailRepoView.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DetailRepoView: View { 11 | 12 | @EnvironmentObject var router: SearchRepoCoordinator.Router 13 | @StateObject var viewModel = DetailRepoViewModel() 14 | 15 | var repo: GithubRepoEntities 16 | 17 | var body: some View { 18 | Text(repo.fullname ?? "") 19 | .navigationTitle(repo.name ?? "") 20 | } 21 | } 22 | 23 | struct DetailRepoView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | DetailRepoView(repo: .init(JSON: [:])!) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Scenes/DetailRepo/DetailRepoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailRepoViewModel.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Combine 11 | import Factory 12 | 13 | class DetailRepoViewModel: ObservableObject { 14 | 15 | let activityIndicator = ActivityIndicator() 16 | let errorTracker = ErrorTracker() 17 | private var bag = Set() 18 | 19 | } 20 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Scenes/SearchRepo/RepoRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoRow.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RepoRow: View { 11 | 12 | var repo: GithubRepoEntities 13 | 14 | var body: some View { 15 | HStack { 16 | VStack(alignment: .leading) { 17 | Text(repo.fullname ?? "") 18 | .font(.headline) 19 | 20 | Text(repo.name ?? "") 21 | .font(.subheadline) 22 | } 23 | Spacer() 24 | } 25 | .contentShape(Rectangle()) 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Scenes/SearchRepo/SearchRepoCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchRepoCoordinator.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import Stinsen 10 | import SwiftUI 11 | 12 | final class SearchRepoCoordinator: NavigationCoordinatable { 13 | 14 | let stack = NavigationStack(initial: \SearchRepoCoordinator.start) 15 | 16 | @Root var start = makeStart 17 | @Route(.push) var pushToDetail = makeDetail 18 | 19 | } 20 | 21 | extension SearchRepoCoordinator { 22 | 23 | @ViewBuilder func makeStart() -> some View { 24 | SearchRepoView() 25 | } 26 | 27 | @ViewBuilder func makeDetail(repo: GithubRepoEntities) -> some View { 28 | DetailRepoView(repo: repo) 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Scenes/SearchRepo/SearchRepoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchRepoView.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | import Stinsen 10 | 11 | struct SearchRepoView: View { 12 | 13 | @StateObject var viewModel = SearchRepoViewModel() 14 | 15 | var body: some View { 16 | List { 17 | ForEach(viewModel.githubRepos, id: \.id) { repo in 18 | RepoRow(repo: repo) 19 | .onTapGesture { 20 | viewModel.pushToDetail(repo: repo) 21 | } 22 | } 23 | } 24 | .searchable(text: $viewModel.searchText) 25 | .onReceiveError(viewModel.errorTracker.errorPublisher) 26 | .onReceiveLoading(viewModel.activityIndicator.isLoadingPublisher) 27 | .navigationTitle("Github repo") 28 | } 29 | } 30 | 31 | struct SearchRepoView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | SearchRepoView() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUI/Scenes/SearchRepo/SearchRepoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchRepoViewModel.swift 3 | // CleanArchitecture-MVVM-C-SwiftUI 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Combine 11 | import Factory 12 | import Stinsen 13 | 14 | class SearchRepoViewModel: ObservableObject { 15 | 16 | let activityIndicator = ActivityIndicator() 17 | let errorTracker = ErrorTracker() 18 | private var bag = Set() 19 | @LazyInjected(\.githubUseCase) var useCase 20 | 21 | @Published var searchText = "" 22 | @Published var githubRepos = [GithubRepoEntities]() 23 | @RouterObject var router: SearchRepoCoordinator.Router? 24 | 25 | init() { 26 | 27 | $searchText 28 | .filter({!$0.isEmpty}) 29 | .removeDuplicates() 30 | .debounce(for: .seconds(1.0), scheduler: DispatchQueue.main) 31 | .flatMap { [self] query in 32 | return useCase.searchRepo(query: query) 33 | .receive(on: DispatchQueue.main) 34 | .trackError(errorTracker) 35 | .trackActivity(activityIndicator) 36 | } 37 | .assign(to: &$githubRepos) 38 | 39 | } 40 | 41 | func pushToDetail(repo: GithubRepoEntities) { 42 | router?.route(to: \.pushToDetail, repo) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUITests/CleanArchitecture_MVVM_C_SwiftUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CleanArchitecture_MVVM_C_SwiftUITests.swift 3 | // CleanArchitecture-MVVM-C-SwiftUITests 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import CleanArchitecture_MVVM_C_SwiftUI 10 | 11 | final class CleanArchitecture_MVVM_C_SwiftUITests: 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 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUIUITests/CleanArchitecture_MVVM_C_SwiftUIUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CleanArchitecture_MVVM_C_SwiftUIUITests.swift 3 | // CleanArchitecture-MVVM-C-SwiftUIUITests 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class CleanArchitecture_MVVM_C_SwiftUIUITests: 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 | -------------------------------------------------------------------------------- /CleanArchitecture-MVVM-C-SwiftUIUITests/CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests.swift 3 | // CleanArchitecture-MVVM-C-SwiftUIUITests 4 | // 5 | // Created by Huy Trinh Duc on 27/05/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class CleanArchitecture_MVVM_C_SwiftUIUITestsLaunchTests: 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 | 2 | # iOS: Clean Architecture với SwiftUI, Combine và MVVM-C 3 | 4 | ## Giới thiệu 5 | **Clean Architecture** là một business architecture, nó tách rời những xử lý nghiệp vụ khỏi **UI** và **framework**. Clean Architecture phân rõ vai trò và trách nhiệm của từng layer trong kiến trúc của mình. 6 | 7 | ## Ưu nhược điểm 8 | Về mặt **ưu điểm**, Clean architecture đạt được: 9 | - Giúp logic nghiệp vụ trở nên rõ ràng. 10 | - Không phụ thuộc vào framework 11 | - Các thành phần UI hoàn toàn tách biệt và độc lập. 12 | - Không phụ thuộc vào nguồn cung cấp dữ liệu. 13 | - Dễ dàng unit test. 14 | 15 | Về mặt **nhược điểm**: 16 | - Clean architecture do phân tách cấu trúc thành nhiều tầng nên dẫn đến việc số lượng code sinh ra là rất lớn. 17 | 18 | ## Các thành phần chính 19 | ![enter image description here](https://raw.githubusercontent.com/sergdort/CleanArchitectureRxSwift/master/Architecture/Modules.png) 20 | 21 | Về mặt cấu trúc, **Clean architecture** gồm 3 thành phần chính: 22 | 23 | - **Domain**: Là tầng chứa các thành phần cơ bản của ứng dụng và những gì ứng dụng có thể làm như các Entity, UseCase,... Nó không phụ thuộc vào bất cứ thành phần nào của UI hay bất kỳ Framework nào và cũng không implement bất kỳ một thành phần nào của ứng dụng tại tầng này. 24 | - **Platform**: Là tầng triển khai các phần cụ thể (concrete implementation) của tầng `Domain`. Tầng `Platform` sẽ che giấu đi những chi tiết được triển khai thực hiện. Bất cứ các task nào liên quan đến `call api, local DB, backend...` sẽ thực hiện ở đây. 25 | - **Application (hoặc Presentation)**: Là tầng chịu trách nhiệm cung cấp thông tin từ ứng dụng cho user và tiếp nhận những input từ user cho ứng dụng. Nó có thể được triển khai với các mô hình như MVC, MVP, MVVM. Đối với SwiftUI thì đây sẽ là nơi chứa các `View`. Trong example project, các `View` hoàn toàn độc lập với tầng `Platform`. Nhiệm vụ duy nhất của một View là "bind" `UI` đến `Domain` để ứng dụng hoạt động. 26 | 27 | ## Chi tiết 28 | **Domain** 29 | `Entities` là các model 30 | ```swift 31 | struct GithubRepoModel: Mappable { 32 | var id: Int? 33 | var name: String? 34 | var fullname: String? 35 | } 36 | ``` 37 | 38 | `UseCase` là nơi xử lý các business logic: Nó có thể sử dụng đến `Repository (ở tầng Platform)` để triển khai các task liên quan đến `api, local DB, backend...` (hoặc **không**) nếu như các use cases là các task không liên quan đến api, db. Tại UseCase sẽ inject **Repository** của tầng `Platform` (hoặc **không**). Như trong ví dụ này thì Repository đã được `inject` vào UseCase bằng lib `Factory`. 39 | 40 | ```swift 41 | protocol GithubRepoUseCaseType { 42 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> 43 | } 44 | 45 | class GithubRepoUseCase: GithubRepoRepositoryType { 46 | @LazyInjected(\.githubRepository) var repository 47 | 48 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> { 49 | repository.searchRepo(query: query) 50 | } 51 | } 52 | ``` 53 | **Platform** 54 | 55 | Tại `Platform` chúng ta sẽ tiến hành triển khai các task như `call api, backend, db` như đã nói ở trên, và tiếp nhận data thông qua một **Repository**. Repository chính là nơi triển khai chi tiết (concrete implementation) các phần cụ thể của những use cases. 56 | 57 | ```swift 58 | protocol GithubRepoRepositoryType { 59 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> 60 | } 61 | 62 | class GithubRepoRepository: GithubRepoRepositoryType { 63 | func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> { 64 | 65 | let param: [String: Any] = [ 66 | "q": query, 67 | "per_page": 10, 68 | "page": 1 69 | ] 70 | 71 | return APIService 72 | .shared 73 | .request(nonBaseResponse: SearchRepoAPIRouter.searchRepo(param: param)) 74 | .tryMap { (response: GithubRepoModel) in 75 | return response.githubRepos ?? [] 76 | } 77 | .eraseToAnyPublisher() 78 | } 79 | } 80 | ``` 81 | **Application** 82 | `Application` là tầng chúng ta sẽ triển khai design pattern `MVVM-C` cùng với `Combine`, khiến việc binding trở nên dễ dàng hơn. Chữ `C` trong cụm từ `MVVM-C` mình sẽ giải thích bên dưới. Tầng `Application` sẽ chỉ dùng đến `UseCase` của `Domain` mà không quan tâm đến những tầng khác. 83 | 84 | ![enter image description here](https://github.com/sergdort/CleanArchitectureRxSwift/blob/master/Architecture/MVVMPattern.png?raw=true) 85 | 86 | `ViewModel` sẽ đóng vai trò chuẩn bị và trung chuyển dữ liệu. 87 | 88 | ViewModel sẽ inject `UseCase` của tầng Domain, chịu trách nhiệm thực hiện các xử lý business logic và `Router` sẽ chịu trách nhiệm điều hướng ứng dụng (chuyển màn hình, show alert,...). Router trong ví dụ sử dụng lib `Stinsen`. 89 | 90 | ```swift 91 | class SearchRepoViewModel: ObservableObject { 92 | 93 | let activityIndicator = ActivityIndicator() 94 | let errorTracker = ErrorTracker() 95 | private var bag = Set() 96 | @LazyInjected(\.githubUseCase) var useCase 97 | 98 | @Published var searchText = "" 99 | @Published var githubRepos = [GithubRepoEntities]() 100 | @RouterObject var router: SearchRepoCoordinator.Router? 101 | 102 | init() { 103 | $searchText 104 | .filter({!$0.isEmpty}) 105 | .removeDuplicates() 106 | .debounce(for: .seconds(1.0), scheduler: DispatchQueue.main) 107 | .flatMap { [self] query in 108 | return useCase.searchRepo(query: query) 109 | .receive(on: DispatchQueue.main) 110 | .trackError(errorTracker) 111 | .trackActivity(activityIndicator) 112 | } 113 | .assign(to: &$githubRepos) 114 | } 115 | 116 | func pushToDetail(repo: GithubRepoEntities) { 117 | router?.route(to: \.pushToDetail, repo) 118 | } 119 | } 120 | 121 | ``` 122 | **Chữ C trong MVVM-C** 123 | 124 | Là `Coordinator`, một design pattern khá phổ biển ở Swift. Coordinator chứa các `Router`, đóng vài trò điều hướng ứng dụng. 125 | 126 | ```swift 127 | final class SearchRepoCoordinator: NavigationCoordinatable { 128 | 129 | let stack = NavigationStack(initial: \SearchRepoCoordinator.start) 130 | 131 | @Root var start = makeStart 132 | @Route(.push) var pushToDetail = makeDetail 133 | 134 | } 135 | 136 | extension SearchRepoCoordinator { 137 | 138 | @ViewBuilder func makeStart() -> some View { 139 | SearchRepoView() 140 | } 141 | 142 | @ViewBuilder func makeDetail(repo: GithubRepoEntities) -> some View { 143 | DetailRepoView(repo: repo) 144 | } 145 | 146 | } 147 | ``` 148 | 149 | Và cuối cùng: `View`, nơi user thao tác, nhận đầu vào và hiển thị các đầu ra tương ứng. 150 | 151 | ```swift 152 | struct SearchRepoView: View { 153 | 154 | @StateObject var viewModel = SearchRepoViewModel() 155 | 156 | var body: some View { 157 | List { 158 | ForEach(viewModel.githubRepos, id: \.id) { repo in 159 | RepoRow(repo: repo) 160 | .onTapGesture { 161 | viewModel.pushToDetail(repo: repo) 162 | } 163 | } 164 | } 165 | .searchable(text: $viewModel.searchText) 166 | .onReceiveError(viewModel.errorTracker.errorPublisher) 167 | .onReceiveLoading(viewModel.activityIndicator.isLoadingPublisher) 168 | .navigationTitle("Github repo") 169 | } 170 | } 171 | ``` 172 | 173 | 174 | ## Biểu đồ luồng chạy của ứng dụng 175 | 176 | Luồng chạy thông qua `Sequence Diagram`: 177 | ```mermaid 178 | sequenceDiagram 179 | SearchRepoView ->> SearchRepoViewModel: 1. $searchText: 180 | SearchRepoViewModel ->> GithubRepoUseCase: 2. searchRepo(query:) (UseCase) 181 | GithubRepoUseCase ->> GithubRepoRepository: 3. searchRepo(query:) (Repository) 182 | GithubRepoRepository ->> API: 4. request() 183 | API -->> GithubRepoRepository: 5. response() 184 | GithubRepoRepository -->> GithubRepoUseCase: 6. Get data success -> parser model 185 | GithubRepoUseCase -->> SearchRepoViewModel: 7. Data 186 | SearchRepoViewModel -->> SearchRepoView: 8. Binding data 187 | ``` 188 | --------------------------------------------------------------------------------