├── .gitignore ├── Podfile ├── README.md ├── SimpleDemoCleanArchitecture.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── SimpleDemoCleanArchitecture.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── SimpleDemoCleanArchitecture ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ ├── Contents.json │ ├── icon_1024@1x.png │ ├── icon_20@2x-1.png │ ├── icon_20@2x.png │ ├── icon_20@3x.png │ ├── icon_29@2x-1.png │ ├── icon_29@2x.png │ ├── icon_29@3x.png │ ├── icon_40@2x-1.png │ ├── icon_40@2x.png │ ├── icon_40@3x.png │ ├── icon_60@2x.png │ └── icon_60@3x.png └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── Domain ├── Architecture │ ├── ActivityIndicator.swift │ ├── BindableType.swift │ ├── ErrorTracker.swift │ ├── MultiActivityIndicator.swift │ └── ViewModelType.swift ├── Extensions │ ├── Driver+.swift │ ├── ObservableType+.swift │ └── UIViewController+Debug.swift └── Models │ ├── GithubOwner.swift │ └── GithubRepo.swift ├── Extensions ├── Array+.swift ├── UIViewController+.swift └── ViewController+Rx.swift ├── Info.plist ├── Platform ├── Repositories │ └── GithubRepoRepository.swift └── Services │ └── API │ ├── APIService.swift │ ├── APITarget.swift │ ├── Request │ └── GithubRepoRequest.swift │ └── Response │ └── GithubRepoResponse.swift ├── Scenes ├── App │ ├── AppNavigator.swift │ ├── AppUseCase.swift │ └── AppViewModel.swift ├── Main │ ├── Cell │ │ ├── GithubRepoCell.swift │ │ └── GithubRepoCell.xib │ ├── MainNavigator.swift │ ├── MainUseCase.swift │ ├── MainViewController.swift │ └── MainViewModel.swift ├── RepoDetail │ ├── RepoDetailNavigator.swift │ ├── RepoDetailUseCase.swift │ ├── RepoDetailViewController.swift │ └── RepoDetailViewModel.swift └── StoryBoards │ ├── Base.lproj │ └── Main.storyboard │ └── StoryBoards.swift └── Utils └── URLs.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | .DS_Store 71 | Podfile.lock 72 | Pods/ 73 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '14.0' 3 | 4 | target 'SimpleDemoCleanArchitecture' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Clean Architecture 9 | pod 'Reusable', '~> 4.1.2' 10 | pod 'Then', '~> 2.7.0' 11 | pod 'SnapKit', '~> 5.0.1' 12 | pod 'Moya/RxSwift', '~> 15.0.0' 13 | pod 'RxAppState', '~> 1.7.1' 14 | pod 'RxDataSources', '~> 5.0.0' 15 | pod 'RxSwift', '~> 6.5.0' 16 | pod 'NSObject+Rx', '~> 5.2.2' 17 | 18 | # Others 19 | pod 'MBProgressHUD', '~> 1.2.0' 20 | pod 'SDWebImage', '~> 5.12.3' 21 | pod 'IQKeyboardManagerSwift', '~> 6.5.9' 22 | end 23 | 24 | post_install do |installer| 25 | installer.generated_projects.each do |project| 26 | project.targets.each do |target| 27 | target.build_configurations.each do |config| 28 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Demo Clean Architecture 2 | Demo project with Clean Architecture, MVVM and RxSwift 3 | 4 | ## RxSwift & RxCocoa Tutorials 5 | 6 | - [RxSwift: Các cách khởi tạo Observable trong RxSwift](https://viblo.asia/p/rxswift-cac-cach-khoi-tao-observable-trong-rxswift-aWj53pkPK6m). 7 | - [RxSwift: Các loại Subject trong RxSwift](https://viblo.asia/p/rxswift-cac-loai-subject-trong-rxswift-Eb85ogym52G). 8 | - [RxSwift: Phân biệt map, flatMap và flatMapLatest trong RxSwift](https://viblo.asia/p/rxswift-phan-biet-map-flatmap-va-flatmaplatest-trong-rxswift-ByEZkp4ylQ0). 9 | - [RxSwift: Combining Operator trong RxSwift](https://viblo.asia/p/rxswift-combining-operator-trong-rxswift-GrLZDvBn5k0). 10 | - [RxSwift: Trait trong RxSwift - Single, Completable, Maybe](https://viblo.asia/p/rxswift-trait-trong-rxswift-single-completable-maybe-L4x5xk2mlBM). 11 | - [RxSwift: Trait trong RxSwift - RxCocoa](https://viblo.asia/p/rxswift-trait-trong-rxswift-rxcocoa-aWj53pa1K6m). 12 | 13 | ## RxSwift Advance 14 | - [RxSwift: KVO - Key Value Observing](https://viblo.asia/p/rxswift-kvo-key-value-observing-ORNZqXa8K0n). 15 | - [RxSwift: Sử dụng Delegate Pattern trong RxSwift với DelegateProxy](https://viblo.asia/p/rxswift-su-dung-delegate-pattern-trong-rxswift-voi-delegateproxy-924lJje0lPM). 16 | 17 | ## MVVM & Clean Architecture 18 | - [RxSwift: Clean Architecture, MVVM và RxSwift (Phần 1)](https://viblo.asia/p/rxswift-clean-architecture-mvvm-va-rxswift-phan-1-gAm5yaR85db). 19 | - [RxSwift: Clean Architecture, MVVM và RxSwift (Phần 2)](https://viblo.asia/p/rxswift-clean-architecture-mvvm-va-rxswift-phan-2-E375zWR6KGW). 20 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 714C2ECD221F83AC001F98FD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2ECC221F83AC001F98FD /* AppDelegate.swift */; }; 11 | 714C2ED2221F83AC001F98FD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 714C2ED0221F83AC001F98FD /* Main.storyboard */; }; 12 | 714C2ED4221F83AE001F98FD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 714C2ED3221F83AE001F98FD /* Assets.xcassets */; }; 13 | 714C2ED7221F83AE001F98FD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 714C2ED5221F83AE001F98FD /* LaunchScreen.storyboard */; }; 14 | 714C2EE1221F86B6001F98FD /* AppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EE0221F86B6001F98FD /* AppViewModel.swift */; }; 15 | 714C2EE3221F86C1001F98FD /* AppNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EE2221F86C1001F98FD /* AppNavigator.swift */; }; 16 | 714C2EE5221F86D0001F98FD /* AppUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EE4221F86D0001F98FD /* AppUseCase.swift */; }; 17 | 714C2EE8221F8951001F98FD /* StoryBoards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EE7221F8951001F98FD /* StoryBoards.swift */; }; 18 | 714C2EEE221F89C7001F98FD /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EED221F89C7001F98FD /* MainViewController.swift */; }; 19 | 714C2EF0221F8EC1001F98FD /* MainNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EEF221F8EC1001F98FD /* MainNavigator.swift */; }; 20 | 714C2EF2221F8ECA001F98FD /* MainUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EF1221F8ECA001F98FD /* MainUseCase.swift */; }; 21 | 714C2EF4221F8ED6001F98FD /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EF3221F8ED6001F98FD /* MainViewModel.swift */; }; 22 | 714C2EF9221F9238001F98FD /* GithubRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2EF8221F9238001F98FD /* GithubRepo.swift */; }; 23 | 714C2F0F221F97F8001F98FD /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F0E221F97F8001F98FD /* URLs.swift */; }; 24 | 714C2F13221F988A001F98FD /* GithubRepoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F12221F988A001F98FD /* GithubRepoResponse.swift */; }; 25 | 714C2F1B221FC7D2001F98FD /* GithubRepoRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F1A221FC7D2001F98FD /* GithubRepoRepository.swift */; }; 26 | 714C2F21221FCAC6001F98FD /* GithubRepoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F1F221FCAC6001F98FD /* GithubRepoCell.swift */; }; 27 | 714C2F22221FCAC6001F98FD /* GithubRepoCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 714C2F20221FCAC6001F98FD /* GithubRepoCell.xib */; }; 28 | 714C2F25221FCD5D001F98FD /* ViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F24221FCD5D001F98FD /* ViewController+Rx.swift */; }; 29 | 714C2F27221FCDE2001F98FD /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F26221FCDE2001F98FD /* UIViewController+.swift */; }; 30 | 714C2F2A221FD75F001F98FD /* RepoDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F29221FD75F001F98FD /* RepoDetailViewModel.swift */; }; 31 | 714C2F2C221FD76E001F98FD /* RepoDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F2B221FD76E001F98FD /* RepoDetailUseCase.swift */; }; 32 | 714C2F2E221FD77D001F98FD /* RepoDetailNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F2D221FD77D001F98FD /* RepoDetailNavigator.swift */; }; 33 | 714C2F30221FD791001F98FD /* RepoDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C2F2F221FD791001F98FD /* RepoDetailViewController.swift */; }; 34 | 9E0BA0B62AD285BA00752733 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 9E0BA0B52AD285BA00752733 /* README.md */; }; 35 | 9E0BA0B92AD2876300752733 /* APITarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0B82AD2876300752733 /* APITarget.swift */; }; 36 | 9E0BA0BB2AD287B200752733 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0BA2AD287B200752733 /* APIService.swift */; }; 37 | 9E0BA0BE2AD2882500752733 /* GithubRepoRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0BD2AD2882500752733 /* GithubRepoRequest.swift */; }; 38 | 9E0BA0D42AD28C2B00752733 /* BindableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0C12AD28C2B00752733 /* BindableType.swift */; }; 39 | 9E0BA0D62AD28C2B00752733 /* ErrorTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0C32AD28C2B00752733 /* ErrorTracker.swift */; }; 40 | 9E0BA0D72AD28C2B00752733 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0C42AD28C2B00752733 /* ActivityIndicator.swift */; }; 41 | 9E0BA0DD2AD28C2B00752733 /* MultiActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0CA2AD28C2B00752733 /* MultiActivityIndicator.swift */; }; 42 | 9E0BA0DE2AD28C2B00752733 /* Driver+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0CC2AD28C2B00752733 /* Driver+.swift */; }; 43 | 9E0BA0DF2AD28C2B00752733 /* ObservableType+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0CD2AD28C2B00752733 /* ObservableType+.swift */; }; 44 | 9E0BA0E02AD28C2B00752733 /* Array+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0CE2AD28C2B00752733 /* Array+.swift */; }; 45 | 9E0BA0E22AD28C2B00752733 /* UIViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0D02AD28C2B00752733 /* UIViewController+Debug.swift */; }; 46 | 9E0BA0E62AD28C8F00752733 /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0E52AD28C8F00752733 /* ViewModelType.swift */; }; 47 | 9E0BA0E82AD28FF600752733 /* GithubOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0BA0E72AD28FF600752733 /* GithubOwner.swift */; }; 48 | AC288A3A913FC0EA073E7B7D /* Pods_SimpleDemoCleanArchitecture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3A01E9CB332F807E2695B53 /* Pods_SimpleDemoCleanArchitecture.framework */; }; 49 | /* End PBXBuildFile section */ 50 | 51 | /* Begin PBXFileReference section */ 52 | 37869304476713634AA3A366 /* Pods-SimpleDemoCleanArchitecture.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleDemoCleanArchitecture.release.xcconfig"; path = "Pods/Target Support Files/Pods-SimpleDemoCleanArchitecture/Pods-SimpleDemoCleanArchitecture.release.xcconfig"; sourceTree = ""; }; 53 | 5BD36DC1B11198D43E3DE2E5 /* Pods-SimpleDemoCleanArchitecture.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleDemoCleanArchitecture.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SimpleDemoCleanArchitecture/Pods-SimpleDemoCleanArchitecture.debug.xcconfig"; sourceTree = ""; }; 54 | 714C2EC9221F83AC001F98FD /* SimpleDemoCleanArchitecture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleDemoCleanArchitecture.app; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 714C2ECC221F83AC001F98FD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 56 | 714C2ED1221F83AC001F98FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 57 | 714C2ED3221F83AE001F98FD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 58 | 714C2ED6221F83AE001F98FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 59 | 714C2ED8221F83AE001F98FD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 60 | 714C2EE0221F86B6001F98FD /* AppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModel.swift; sourceTree = ""; }; 61 | 714C2EE2221F86C1001F98FD /* AppNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigator.swift; sourceTree = ""; }; 62 | 714C2EE4221F86D0001F98FD /* AppUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUseCase.swift; sourceTree = ""; }; 63 | 714C2EE7221F8951001F98FD /* StoryBoards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryBoards.swift; sourceTree = ""; }; 64 | 714C2EED221F89C7001F98FD /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 65 | 714C2EEF221F8EC1001F98FD /* MainNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigator.swift; sourceTree = ""; }; 66 | 714C2EF1221F8ECA001F98FD /* MainUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUseCase.swift; sourceTree = ""; }; 67 | 714C2EF3221F8ED6001F98FD /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; 68 | 714C2EF8221F9238001F98FD /* GithubRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepo.swift; sourceTree = ""; }; 69 | 714C2F0E221F97F8001F98FD /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; 70 | 714C2F12221F988A001F98FD /* GithubRepoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepoResponse.swift; sourceTree = ""; }; 71 | 714C2F1A221FC7D2001F98FD /* GithubRepoRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepoRepository.swift; sourceTree = ""; }; 72 | 714C2F1F221FCAC6001F98FD /* GithubRepoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepoCell.swift; sourceTree = ""; }; 73 | 714C2F20221FCAC6001F98FD /* GithubRepoCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GithubRepoCell.xib; sourceTree = ""; }; 74 | 714C2F24221FCD5D001F98FD /* ViewController+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+Rx.swift"; sourceTree = ""; }; 75 | 714C2F26221FCDE2001F98FD /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; 76 | 714C2F29221FD75F001F98FD /* RepoDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoDetailViewModel.swift; sourceTree = ""; }; 77 | 714C2F2B221FD76E001F98FD /* RepoDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoDetailUseCase.swift; sourceTree = ""; }; 78 | 714C2F2D221FD77D001F98FD /* RepoDetailNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoDetailNavigator.swift; sourceTree = ""; }; 79 | 714C2F2F221FD791001F98FD /* RepoDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoDetailViewController.swift; sourceTree = ""; }; 80 | 9E0BA0B52AD285BA00752733 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 81 | 9E0BA0B82AD2876300752733 /* APITarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITarget.swift; sourceTree = ""; }; 82 | 9E0BA0BA2AD287B200752733 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 83 | 9E0BA0BD2AD2882500752733 /* GithubRepoRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubRepoRequest.swift; sourceTree = ""; }; 84 | 9E0BA0C12AD28C2B00752733 /* BindableType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BindableType.swift; sourceTree = ""; }; 85 | 9E0BA0C32AD28C2B00752733 /* ErrorTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorTracker.swift; sourceTree = ""; }; 86 | 9E0BA0C42AD28C2B00752733 /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 87 | 9E0BA0CA2AD28C2B00752733 /* MultiActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiActivityIndicator.swift; sourceTree = ""; }; 88 | 9E0BA0CC2AD28C2B00752733 /* Driver+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Driver+.swift"; sourceTree = ""; }; 89 | 9E0BA0CD2AD28C2B00752733 /* ObservableType+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ObservableType+.swift"; sourceTree = ""; }; 90 | 9E0BA0CE2AD28C2B00752733 /* Array+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+.swift"; sourceTree = ""; }; 91 | 9E0BA0D02AD28C2B00752733 /* UIViewController+Debug.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Debug.swift"; sourceTree = ""; }; 92 | 9E0BA0E52AD28C8F00752733 /* ViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; }; 93 | 9E0BA0E72AD28FF600752733 /* GithubOwner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubOwner.swift; sourceTree = ""; }; 94 | A3A01E9CB332F807E2695B53 /* Pods_SimpleDemoCleanArchitecture.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SimpleDemoCleanArchitecture.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 95 | /* End PBXFileReference section */ 96 | 97 | /* Begin PBXFrameworksBuildPhase section */ 98 | 714C2EC6221F83AC001F98FD /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | AC288A3A913FC0EA073E7B7D /* Pods_SimpleDemoCleanArchitecture.framework in Frameworks */, 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | 714C2EC0221F83AC001F98FD = { 110 | isa = PBXGroup; 111 | children = ( 112 | 714C2ECB221F83AC001F98FD /* SimpleDemoCleanArchitecture */, 113 | 714C2ECA221F83AC001F98FD /* Products */, 114 | B31819743BD4361FAF650F86 /* Pods */, 115 | B2EC6C500E801CBB1BBAB9E5 /* Frameworks */, 116 | ); 117 | sourceTree = ""; 118 | }; 119 | 714C2ECA221F83AC001F98FD /* Products */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 714C2EC9221F83AC001F98FD /* SimpleDemoCleanArchitecture.app */, 123 | ); 124 | name = Products; 125 | sourceTree = ""; 126 | }; 127 | 714C2ECB221F83AC001F98FD /* SimpleDemoCleanArchitecture */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 9E0BA0B52AD285BA00752733 /* README.md */, 131 | 714C2F09221F971E001F98FD /* Utils */, 132 | 714C2F23221FCD51001F98FD /* Extensions */, 133 | 714C2EF5221F91A9001F98FD /* Domain */, 134 | 714C2EF6221F91AE001F98FD /* Platform */, 135 | 714C2EDE221F868C001F98FD /* Scenes */, 136 | 714C2ECC221F83AC001F98FD /* AppDelegate.swift */, 137 | 714C2ED3221F83AE001F98FD /* Assets.xcassets */, 138 | 714C2ED5221F83AE001F98FD /* LaunchScreen.storyboard */, 139 | 714C2ED8221F83AE001F98FD /* Info.plist */, 140 | ); 141 | path = SimpleDemoCleanArchitecture; 142 | sourceTree = ""; 143 | }; 144 | 714C2EDE221F868C001F98FD /* Scenes */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 714C2F28221FD739001F98FD /* RepoDetail */, 148 | 714C2EE9221F8999001F98FD /* Main */, 149 | 714C2EDF221F86A2001F98FD /* App */, 150 | 714C2EE6221F893C001F98FD /* StoryBoards */, 151 | ); 152 | path = Scenes; 153 | sourceTree = ""; 154 | }; 155 | 714C2EDF221F86A2001F98FD /* App */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 714C2EE0221F86B6001F98FD /* AppViewModel.swift */, 159 | 714C2EE2221F86C1001F98FD /* AppNavigator.swift */, 160 | 714C2EE4221F86D0001F98FD /* AppUseCase.swift */, 161 | ); 162 | path = App; 163 | sourceTree = ""; 164 | }; 165 | 714C2EE6221F893C001F98FD /* StoryBoards */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 714C2EE7221F8951001F98FD /* StoryBoards.swift */, 169 | 714C2ED0221F83AC001F98FD /* Main.storyboard */, 170 | ); 171 | path = StoryBoards; 172 | sourceTree = ""; 173 | }; 174 | 714C2EE9221F8999001F98FD /* Main */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 714C2F1E221FCA9E001F98FD /* Cell */, 178 | 714C2EED221F89C7001F98FD /* MainViewController.swift */, 179 | 714C2EF3221F8ED6001F98FD /* MainViewModel.swift */, 180 | 714C2EEF221F8EC1001F98FD /* MainNavigator.swift */, 181 | 714C2EF1221F8ECA001F98FD /* MainUseCase.swift */, 182 | ); 183 | path = Main; 184 | sourceTree = ""; 185 | }; 186 | 714C2EF5221F91A9001F98FD /* Domain */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | 9E0BA0BF2AD28C2B00752733 /* Architecture */, 190 | 9E0BA0CB2AD28C2B00752733 /* Extensions */, 191 | 714C2EF7221F922A001F98FD /* Models */, 192 | ); 193 | path = Domain; 194 | sourceTree = ""; 195 | }; 196 | 714C2EF6221F91AE001F98FD /* Platform */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 714C2EFB221F92D9001F98FD /* Services */, 200 | 714C2EFA221F92C3001F98FD /* Repositories */, 201 | ); 202 | path = Platform; 203 | sourceTree = ""; 204 | }; 205 | 714C2EF7221F922A001F98FD /* Models */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 714C2EF8221F9238001F98FD /* GithubRepo.swift */, 209 | 9E0BA0E72AD28FF600752733 /* GithubOwner.swift */, 210 | ); 211 | path = Models; 212 | sourceTree = ""; 213 | }; 214 | 714C2EFA221F92C3001F98FD /* Repositories */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 714C2F1A221FC7D2001F98FD /* GithubRepoRepository.swift */, 218 | ); 219 | path = Repositories; 220 | sourceTree = ""; 221 | }; 222 | 714C2EFB221F92D9001F98FD /* Services */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | 714C2F03221F94E0001F98FD /* API */, 226 | ); 227 | path = Services; 228 | sourceTree = ""; 229 | }; 230 | 714C2F03221F94E0001F98FD /* API */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | 9E0BA0B82AD2876300752733 /* APITarget.swift */, 234 | 9E0BA0BA2AD287B200752733 /* APIService.swift */, 235 | 9E0BA0BC2AD2882500752733 /* Request */, 236 | 714C2F05221F96D5001F98FD /* Response */, 237 | ); 238 | path = API; 239 | sourceTree = ""; 240 | }; 241 | 714C2F05221F96D5001F98FD /* Response */ = { 242 | isa = PBXGroup; 243 | children = ( 244 | 714C2F12221F988A001F98FD /* GithubRepoResponse.swift */, 245 | ); 246 | path = Response; 247 | sourceTree = ""; 248 | }; 249 | 714C2F09221F971E001F98FD /* Utils */ = { 250 | isa = PBXGroup; 251 | children = ( 252 | 714C2F0E221F97F8001F98FD /* URLs.swift */, 253 | ); 254 | path = Utils; 255 | sourceTree = ""; 256 | }; 257 | 714C2F1E221FCA9E001F98FD /* Cell */ = { 258 | isa = PBXGroup; 259 | children = ( 260 | 714C2F20221FCAC6001F98FD /* GithubRepoCell.xib */, 261 | 714C2F1F221FCAC6001F98FD /* GithubRepoCell.swift */, 262 | ); 263 | path = Cell; 264 | sourceTree = ""; 265 | }; 266 | 714C2F23221FCD51001F98FD /* Extensions */ = { 267 | isa = PBXGroup; 268 | children = ( 269 | 714C2F24221FCD5D001F98FD /* ViewController+Rx.swift */, 270 | 714C2F26221FCDE2001F98FD /* UIViewController+.swift */, 271 | 9E0BA0CE2AD28C2B00752733 /* Array+.swift */, 272 | ); 273 | path = Extensions; 274 | sourceTree = ""; 275 | }; 276 | 714C2F28221FD739001F98FD /* RepoDetail */ = { 277 | isa = PBXGroup; 278 | children = ( 279 | 714C2F2F221FD791001F98FD /* RepoDetailViewController.swift */, 280 | 714C2F29221FD75F001F98FD /* RepoDetailViewModel.swift */, 281 | 714C2F2D221FD77D001F98FD /* RepoDetailNavigator.swift */, 282 | 714C2F2B221FD76E001F98FD /* RepoDetailUseCase.swift */, 283 | ); 284 | path = RepoDetail; 285 | sourceTree = ""; 286 | }; 287 | 9E0BA0BC2AD2882500752733 /* Request */ = { 288 | isa = PBXGroup; 289 | children = ( 290 | 9E0BA0BD2AD2882500752733 /* GithubRepoRequest.swift */, 291 | ); 292 | path = Request; 293 | sourceTree = ""; 294 | }; 295 | 9E0BA0BF2AD28C2B00752733 /* Architecture */ = { 296 | isa = PBXGroup; 297 | children = ( 298 | 9E0BA0E52AD28C8F00752733 /* ViewModelType.swift */, 299 | 9E0BA0C12AD28C2B00752733 /* BindableType.swift */, 300 | 9E0BA0C32AD28C2B00752733 /* ErrorTracker.swift */, 301 | 9E0BA0C42AD28C2B00752733 /* ActivityIndicator.swift */, 302 | 9E0BA0CA2AD28C2B00752733 /* MultiActivityIndicator.swift */, 303 | ); 304 | path = Architecture; 305 | sourceTree = ""; 306 | }; 307 | 9E0BA0CB2AD28C2B00752733 /* Extensions */ = { 308 | isa = PBXGroup; 309 | children = ( 310 | 9E0BA0CC2AD28C2B00752733 /* Driver+.swift */, 311 | 9E0BA0CD2AD28C2B00752733 /* ObservableType+.swift */, 312 | 9E0BA0D02AD28C2B00752733 /* UIViewController+Debug.swift */, 313 | ); 314 | path = Extensions; 315 | sourceTree = ""; 316 | }; 317 | B2EC6C500E801CBB1BBAB9E5 /* Frameworks */ = { 318 | isa = PBXGroup; 319 | children = ( 320 | A3A01E9CB332F807E2695B53 /* Pods_SimpleDemoCleanArchitecture.framework */, 321 | ); 322 | name = Frameworks; 323 | sourceTree = ""; 324 | }; 325 | B31819743BD4361FAF650F86 /* Pods */ = { 326 | isa = PBXGroup; 327 | children = ( 328 | 5BD36DC1B11198D43E3DE2E5 /* Pods-SimpleDemoCleanArchitecture.debug.xcconfig */, 329 | 37869304476713634AA3A366 /* Pods-SimpleDemoCleanArchitecture.release.xcconfig */, 330 | ); 331 | name = Pods; 332 | sourceTree = ""; 333 | }; 334 | /* End PBXGroup section */ 335 | 336 | /* Begin PBXNativeTarget section */ 337 | 714C2EC8221F83AC001F98FD /* SimpleDemoCleanArchitecture */ = { 338 | isa = PBXNativeTarget; 339 | buildConfigurationList = 714C2EDB221F83AE001F98FD /* Build configuration list for PBXNativeTarget "SimpleDemoCleanArchitecture" */; 340 | buildPhases = ( 341 | D35FB4CA8BCBC1697A8082AC /* [CP] Check Pods Manifest.lock */, 342 | 714C2EC5221F83AC001F98FD /* Sources */, 343 | 714C2EC6221F83AC001F98FD /* Frameworks */, 344 | 714C2EC7221F83AC001F98FD /* Resources */, 345 | FF1260FE5E85DC2F82C47BB7 /* [CP] Embed Pods Frameworks */, 346 | ); 347 | buildRules = ( 348 | ); 349 | dependencies = ( 350 | ); 351 | name = SimpleDemoCleanArchitecture; 352 | productName = SimpleDemoCleanArchitecture; 353 | productReference = 714C2EC9221F83AC001F98FD /* SimpleDemoCleanArchitecture.app */; 354 | productType = "com.apple.product-type.application"; 355 | }; 356 | /* End PBXNativeTarget section */ 357 | 358 | /* Begin PBXProject section */ 359 | 714C2EC1221F83AC001F98FD /* Project object */ = { 360 | isa = PBXProject; 361 | attributes = { 362 | LastSwiftUpdateCheck = 1010; 363 | LastUpgradeCheck = 1010; 364 | ORGANIZATIONNAME = trinh.giang.dong; 365 | TargetAttributes = { 366 | 714C2EC8221F83AC001F98FD = { 367 | CreatedOnToolsVersion = 10.1; 368 | ProvisioningStyle = Automatic; 369 | }; 370 | }; 371 | }; 372 | buildConfigurationList = 714C2EC4221F83AC001F98FD /* Build configuration list for PBXProject "SimpleDemoCleanArchitecture" */; 373 | compatibilityVersion = "Xcode 8.0"; 374 | developmentRegion = en; 375 | hasScannedForEncodings = 0; 376 | knownRegions = ( 377 | en, 378 | Base, 379 | ); 380 | mainGroup = 714C2EC0221F83AC001F98FD; 381 | productRefGroup = 714C2ECA221F83AC001F98FD /* Products */; 382 | projectDirPath = ""; 383 | projectRoot = ""; 384 | targets = ( 385 | 714C2EC8221F83AC001F98FD /* SimpleDemoCleanArchitecture */, 386 | ); 387 | }; 388 | /* End PBXProject section */ 389 | 390 | /* Begin PBXResourcesBuildPhase section */ 391 | 714C2EC7221F83AC001F98FD /* Resources */ = { 392 | isa = PBXResourcesBuildPhase; 393 | buildActionMask = 2147483647; 394 | files = ( 395 | 714C2F22221FCAC6001F98FD /* GithubRepoCell.xib in Resources */, 396 | 714C2ED7221F83AE001F98FD /* LaunchScreen.storyboard in Resources */, 397 | 9E0BA0B62AD285BA00752733 /* README.md in Resources */, 398 | 714C2ED4221F83AE001F98FD /* Assets.xcassets in Resources */, 399 | 714C2ED2221F83AC001F98FD /* Main.storyboard in Resources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | /* End PBXResourcesBuildPhase section */ 404 | 405 | /* Begin PBXShellScriptBuildPhase section */ 406 | D35FB4CA8BCBC1697A8082AC /* [CP] Check Pods Manifest.lock */ = { 407 | isa = PBXShellScriptBuildPhase; 408 | buildActionMask = 2147483647; 409 | files = ( 410 | ); 411 | inputFileListPaths = ( 412 | ); 413 | inputPaths = ( 414 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 415 | "${PODS_ROOT}/Manifest.lock", 416 | ); 417 | name = "[CP] Check Pods Manifest.lock"; 418 | outputFileListPaths = ( 419 | ); 420 | outputPaths = ( 421 | "$(DERIVED_FILE_DIR)/Pods-SimpleDemoCleanArchitecture-checkManifestLockResult.txt", 422 | ); 423 | runOnlyForDeploymentPostprocessing = 0; 424 | shellPath = /bin/sh; 425 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 426 | showEnvVarsInLog = 0; 427 | }; 428 | FF1260FE5E85DC2F82C47BB7 /* [CP] Embed Pods Frameworks */ = { 429 | isa = PBXShellScriptBuildPhase; 430 | buildActionMask = 2147483647; 431 | files = ( 432 | ); 433 | inputPaths = ( 434 | "${PODS_ROOT}/Target Support Files/Pods-SimpleDemoCleanArchitecture/Pods-SimpleDemoCleanArchitecture-frameworks.sh", 435 | "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", 436 | "${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework", 437 | "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework", 438 | "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework", 439 | "${BUILT_PRODUCTS_DIR}/Moya/Moya.framework", 440 | "${BUILT_PRODUCTS_DIR}/NSObject+Rx/NSObject_Rx.framework", 441 | "${BUILT_PRODUCTS_DIR}/Reusable/Reusable.framework", 442 | "${BUILT_PRODUCTS_DIR}/RxAppState/RxAppState.framework", 443 | "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", 444 | "${BUILT_PRODUCTS_DIR}/RxDataSources/RxDataSources.framework", 445 | "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework", 446 | "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", 447 | "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", 448 | "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", 449 | "${BUILT_PRODUCTS_DIR}/Then/Then.framework", 450 | ); 451 | name = "[CP] Embed Pods Frameworks"; 452 | outputPaths = ( 453 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", 454 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Differentiator.framework", 455 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework", 456 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MBProgressHUD.framework", 457 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Moya.framework", 458 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NSObject_Rx.framework", 459 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reusable.framework", 460 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAppState.framework", 461 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", 462 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxDataSources.framework", 463 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRelay.framework", 464 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", 465 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", 466 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", 467 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Then.framework", 468 | ); 469 | runOnlyForDeploymentPostprocessing = 0; 470 | shellPath = /bin/sh; 471 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SimpleDemoCleanArchitecture/Pods-SimpleDemoCleanArchitecture-frameworks.sh\"\n"; 472 | showEnvVarsInLog = 0; 473 | }; 474 | /* End PBXShellScriptBuildPhase section */ 475 | 476 | /* Begin PBXSourcesBuildPhase section */ 477 | 714C2EC5221F83AC001F98FD /* Sources */ = { 478 | isa = PBXSourcesBuildPhase; 479 | buildActionMask = 2147483647; 480 | files = ( 481 | 714C2EEE221F89C7001F98FD /* MainViewController.swift in Sources */, 482 | 714C2EE8221F8951001F98FD /* StoryBoards.swift in Sources */, 483 | 714C2F0F221F97F8001F98FD /* URLs.swift in Sources */, 484 | 9E0BA0DE2AD28C2B00752733 /* Driver+.swift in Sources */, 485 | 9E0BA0E02AD28C2B00752733 /* Array+.swift in Sources */, 486 | 714C2F2C221FD76E001F98FD /* RepoDetailUseCase.swift in Sources */, 487 | 9E0BA0D72AD28C2B00752733 /* ActivityIndicator.swift in Sources */, 488 | 9E0BA0BE2AD2882500752733 /* GithubRepoRequest.swift in Sources */, 489 | 9E0BA0DD2AD28C2B00752733 /* MultiActivityIndicator.swift in Sources */, 490 | 714C2EF9221F9238001F98FD /* GithubRepo.swift in Sources */, 491 | 714C2F1B221FC7D2001F98FD /* GithubRepoRepository.swift in Sources */, 492 | 714C2F2E221FD77D001F98FD /* RepoDetailNavigator.swift in Sources */, 493 | 9E0BA0E62AD28C8F00752733 /* ViewModelType.swift in Sources */, 494 | 714C2EE3221F86C1001F98FD /* AppNavigator.swift in Sources */, 495 | 714C2EE5221F86D0001F98FD /* AppUseCase.swift in Sources */, 496 | 714C2F30221FD791001F98FD /* RepoDetailViewController.swift in Sources */, 497 | 714C2F2A221FD75F001F98FD /* RepoDetailViewModel.swift in Sources */, 498 | 714C2F25221FCD5D001F98FD /* ViewController+Rx.swift in Sources */, 499 | 9E0BA0E82AD28FF600752733 /* GithubOwner.swift in Sources */, 500 | 9E0BA0D42AD28C2B00752733 /* BindableType.swift in Sources */, 501 | 714C2EF4221F8ED6001F98FD /* MainViewModel.swift in Sources */, 502 | 714C2ECD221F83AC001F98FD /* AppDelegate.swift in Sources */, 503 | 9E0BA0E22AD28C2B00752733 /* UIViewController+Debug.swift in Sources */, 504 | 9E0BA0B92AD2876300752733 /* APITarget.swift in Sources */, 505 | 714C2F21221FCAC6001F98FD /* GithubRepoCell.swift in Sources */, 506 | 714C2F13221F988A001F98FD /* GithubRepoResponse.swift in Sources */, 507 | 714C2EF0221F8EC1001F98FD /* MainNavigator.swift in Sources */, 508 | 714C2F27221FCDE2001F98FD /* UIViewController+.swift in Sources */, 509 | 9E0BA0D62AD28C2B00752733 /* ErrorTracker.swift in Sources */, 510 | 714C2EE1221F86B6001F98FD /* AppViewModel.swift in Sources */, 511 | 9E0BA0DF2AD28C2B00752733 /* ObservableType+.swift in Sources */, 512 | 9E0BA0BB2AD287B200752733 /* APIService.swift in Sources */, 513 | 714C2EF2221F8ECA001F98FD /* MainUseCase.swift in Sources */, 514 | ); 515 | runOnlyForDeploymentPostprocessing = 0; 516 | }; 517 | /* End PBXSourcesBuildPhase section */ 518 | 519 | /* Begin PBXVariantGroup section */ 520 | 714C2ED0221F83AC001F98FD /* Main.storyboard */ = { 521 | isa = PBXVariantGroup; 522 | children = ( 523 | 714C2ED1221F83AC001F98FD /* Base */, 524 | ); 525 | name = Main.storyboard; 526 | sourceTree = ""; 527 | }; 528 | 714C2ED5221F83AE001F98FD /* LaunchScreen.storyboard */ = { 529 | isa = PBXVariantGroup; 530 | children = ( 531 | 714C2ED6221F83AE001F98FD /* Base */, 532 | ); 533 | name = LaunchScreen.storyboard; 534 | sourceTree = ""; 535 | }; 536 | /* End PBXVariantGroup section */ 537 | 538 | /* Begin XCBuildConfiguration section */ 539 | 714C2ED9221F83AE001F98FD /* Debug */ = { 540 | isa = XCBuildConfiguration; 541 | buildSettings = { 542 | ALWAYS_SEARCH_USER_PATHS = NO; 543 | CLANG_ANALYZER_NONNULL = YES; 544 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 545 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 546 | CLANG_CXX_LIBRARY = "libc++"; 547 | CLANG_ENABLE_MODULES = YES; 548 | CLANG_ENABLE_OBJC_ARC = YES; 549 | CLANG_ENABLE_OBJC_WEAK = YES; 550 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 551 | CLANG_WARN_BOOL_CONVERSION = YES; 552 | CLANG_WARN_COMMA = YES; 553 | CLANG_WARN_CONSTANT_CONVERSION = YES; 554 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 555 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 556 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 557 | CLANG_WARN_EMPTY_BODY = YES; 558 | CLANG_WARN_ENUM_CONVERSION = YES; 559 | CLANG_WARN_INFINITE_RECURSION = YES; 560 | CLANG_WARN_INT_CONVERSION = YES; 561 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 562 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 563 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 564 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 565 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 566 | CLANG_WARN_STRICT_PROTOTYPES = YES; 567 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 568 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 569 | CLANG_WARN_UNREACHABLE_CODE = YES; 570 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 571 | CODE_SIGN_IDENTITY = "iPhone Developer"; 572 | COPY_PHASE_STRIP = NO; 573 | DEBUG_INFORMATION_FORMAT = dwarf; 574 | ENABLE_STRICT_OBJC_MSGSEND = YES; 575 | ENABLE_TESTABILITY = YES; 576 | GCC_C_LANGUAGE_STANDARD = gnu11; 577 | GCC_DYNAMIC_NO_PIC = NO; 578 | GCC_NO_COMMON_BLOCKS = YES; 579 | GCC_OPTIMIZATION_LEVEL = 0; 580 | GCC_PREPROCESSOR_DEFINITIONS = ( 581 | "DEBUG=1", 582 | "$(inherited)", 583 | ); 584 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 585 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 586 | GCC_WARN_UNDECLARED_SELECTOR = YES; 587 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 588 | GCC_WARN_UNUSED_FUNCTION = YES; 589 | GCC_WARN_UNUSED_VARIABLE = YES; 590 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 591 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 592 | MTL_FAST_MATH = YES; 593 | ONLY_ACTIVE_ARCH = YES; 594 | SDKROOT = iphoneos; 595 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 596 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 597 | }; 598 | name = Debug; 599 | }; 600 | 714C2EDA221F83AE001F98FD /* Release */ = { 601 | isa = XCBuildConfiguration; 602 | buildSettings = { 603 | ALWAYS_SEARCH_USER_PATHS = NO; 604 | CLANG_ANALYZER_NONNULL = YES; 605 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 606 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 607 | CLANG_CXX_LIBRARY = "libc++"; 608 | CLANG_ENABLE_MODULES = YES; 609 | CLANG_ENABLE_OBJC_ARC = YES; 610 | CLANG_ENABLE_OBJC_WEAK = YES; 611 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 612 | CLANG_WARN_BOOL_CONVERSION = YES; 613 | CLANG_WARN_COMMA = YES; 614 | CLANG_WARN_CONSTANT_CONVERSION = YES; 615 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 616 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 617 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 618 | CLANG_WARN_EMPTY_BODY = YES; 619 | CLANG_WARN_ENUM_CONVERSION = YES; 620 | CLANG_WARN_INFINITE_RECURSION = YES; 621 | CLANG_WARN_INT_CONVERSION = YES; 622 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 623 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 624 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 625 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 626 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 627 | CLANG_WARN_STRICT_PROTOTYPES = YES; 628 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 629 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 630 | CLANG_WARN_UNREACHABLE_CODE = YES; 631 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 632 | CODE_SIGN_IDENTITY = "iPhone Developer"; 633 | COPY_PHASE_STRIP = NO; 634 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 635 | ENABLE_NS_ASSERTIONS = NO; 636 | ENABLE_STRICT_OBJC_MSGSEND = YES; 637 | GCC_C_LANGUAGE_STANDARD = gnu11; 638 | GCC_NO_COMMON_BLOCKS = YES; 639 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 640 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 641 | GCC_WARN_UNDECLARED_SELECTOR = YES; 642 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 643 | GCC_WARN_UNUSED_FUNCTION = YES; 644 | GCC_WARN_UNUSED_VARIABLE = YES; 645 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 646 | MTL_ENABLE_DEBUG_INFO = NO; 647 | MTL_FAST_MATH = YES; 648 | SDKROOT = iphoneos; 649 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 650 | VALIDATE_PRODUCT = YES; 651 | }; 652 | name = Release; 653 | }; 654 | 714C2EDC221F83AE001F98FD /* Debug */ = { 655 | isa = XCBuildConfiguration; 656 | baseConfigurationReference = 5BD36DC1B11198D43E3DE2E5 /* Pods-SimpleDemoCleanArchitecture.debug.xcconfig */; 657 | buildSettings = { 658 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 659 | CODE_SIGN_STYLE = Automatic; 660 | INFOPLIST_FILE = SimpleDemoCleanArchitecture/Info.plist; 661 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 662 | PRODUCT_BUNDLE_IDENTIFIER = com.example.SimpleDemoCleanArchitecture; 663 | PRODUCT_NAME = "$(TARGET_NAME)"; 664 | SWIFT_VERSION = 5.0; 665 | TARGETED_DEVICE_FAMILY = "1,2"; 666 | }; 667 | name = Debug; 668 | }; 669 | 714C2EDD221F83AE001F98FD /* Release */ = { 670 | isa = XCBuildConfiguration; 671 | baseConfigurationReference = 37869304476713634AA3A366 /* Pods-SimpleDemoCleanArchitecture.release.xcconfig */; 672 | buildSettings = { 673 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 674 | CODE_SIGN_STYLE = Automatic; 675 | INFOPLIST_FILE = SimpleDemoCleanArchitecture/Info.plist; 676 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 677 | PRODUCT_BUNDLE_IDENTIFIER = com.example.SimpleDemoCleanArchitecture; 678 | PRODUCT_NAME = "$(TARGET_NAME)"; 679 | SWIFT_VERSION = 5.0; 680 | TARGETED_DEVICE_FAMILY = "1,2"; 681 | }; 682 | name = Release; 683 | }; 684 | /* End XCBuildConfiguration section */ 685 | 686 | /* Begin XCConfigurationList section */ 687 | 714C2EC4221F83AC001F98FD /* Build configuration list for PBXProject "SimpleDemoCleanArchitecture" */ = { 688 | isa = XCConfigurationList; 689 | buildConfigurations = ( 690 | 714C2ED9221F83AE001F98FD /* Debug */, 691 | 714C2EDA221F83AE001F98FD /* Release */, 692 | ); 693 | defaultConfigurationIsVisible = 0; 694 | defaultConfigurationName = Release; 695 | }; 696 | 714C2EDB221F83AE001F98FD /* Build configuration list for PBXNativeTarget "SimpleDemoCleanArchitecture" */ = { 697 | isa = XCConfigurationList; 698 | buildConfigurations = ( 699 | 714C2EDC221F83AE001F98FD /* Debug */, 700 | 714C2EDD221F83AE001F98FD /* Release */, 701 | ); 702 | defaultConfigurationIsVisible = 0; 703 | defaultConfigurationName = Release; 704 | }; 705 | /* End XCConfigurationList section */ 706 | }; 707 | rootObject = 714C2EC1221F83AC001F98FD /* Project object */; 708 | } 709 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import NSObject_Rx 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | var disposeBag = DisposeBag() 18 | var window: UIWindow? 19 | 20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | 22 | bindViewModel() 23 | 24 | return true 25 | } 26 | 27 | private func bindViewModel() { 28 | guard let window = window else { return } 29 | let navigator = AppNavigator(window: window) 30 | let useCase = AppUseCase() 31 | let viewModel = AppViewModel(navigator: navigator, useCase: useCase) 32 | 33 | let input = AppViewModel.Input(loadTrigger: Driver.just(())) 34 | let _ = viewModel.transform(input, disposeBag: disposeBag) 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon_20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon_20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon_29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon_29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon_40@2x-1.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon_40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon_60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon_60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "size" : "20x20", 54 | "scale" : "1x" 55 | }, 56 | { 57 | "size" : "20x20", 58 | "idiom" : "ipad", 59 | "filename" : "icon_20@2x-1.png", 60 | "scale" : "2x" 61 | }, 62 | { 63 | "idiom" : "ipad", 64 | "size" : "29x29", 65 | "scale" : "1x" 66 | }, 67 | { 68 | "size" : "29x29", 69 | "idiom" : "ipad", 70 | "filename" : "icon_29@2x-1.png", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "40x40", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "size" : "40x40", 80 | "idiom" : "ipad", 81 | "filename" : "icon_40@2x.png", 82 | "scale" : "2x" 83 | }, 84 | { 85 | "idiom" : "ipad", 86 | "size" : "76x76", 87 | "scale" : "1x" 88 | }, 89 | { 90 | "idiom" : "ipad", 91 | "size" : "76x76", 92 | "scale" : "2x" 93 | }, 94 | { 95 | "idiom" : "ipad", 96 | "size" : "83.5x83.5", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "1024x1024", 101 | "idiom" : "ios-marketing", 102 | "filename" : "icon_1024@1x.png", 103 | "scale" : "1x" 104 | } 105 | ], 106 | "info" : { 107 | "version" : 1, 108 | "author" : "xcode" 109 | } 110 | } -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_1024@1x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_20@2x-1.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_20@2x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_20@3x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_29@2x-1.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_29@2x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_29@3x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_40@2x-1.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_40@2x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_40@3x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_60@2x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgdong2296/SimpleDemoCleanArchitecture/0b8a5b86fa63609f43f6801361f5bc4a306638c7/SimpleDemoCleanArchitecture/Assets.xcassets/AppIcon.appiconset/icon_60@3x.png -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Architecture/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | class ActivityIndicator: SharedSequenceConvertibleType { 6 | typealias Element = Bool 7 | typealias SharingStrategy = DriverSharingStrategy 8 | 9 | private let _lock = NSRecursiveLock() 10 | private let _variable = BehaviorRelay(value: false) 11 | private let _loading: SharedSequence 12 | 13 | init() { 14 | _loading = _variable.asDriver() 15 | .distinctUntilChanged() 16 | } 17 | 18 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 19 | return source.asObservable() 20 | .do(onNext: { _ in 21 | self.sendStopLoading() 22 | }, onError: { _ in 23 | self.sendStopLoading() 24 | }, onCompleted: { 25 | self.sendStopLoading() 26 | }, onSubscribe: subscribed) 27 | } 28 | 29 | private func subscribed() { 30 | _lock.lock() 31 | _variable.accept(true) 32 | _lock.unlock() 33 | } 34 | 35 | private func sendStopLoading() { 36 | _lock.lock() 37 | _variable.accept(false) 38 | _lock.unlock() 39 | } 40 | 41 | func asSharedSequence() -> SharedSequence { 42 | return _loading 43 | } 44 | } 45 | 46 | extension ObservableConvertibleType { 47 | func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { 48 | return activityIndicator.trackActivityOfObservable(self) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Architecture/BindableType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | protocol BindableType: AnyObject { 6 | associatedtype ViewModelType 7 | 8 | var disposeBag: DisposeBag! { get set } 9 | var viewModel: ViewModelType! { get set } 10 | 11 | func bindViewModel() 12 | } 13 | 14 | extension BindableType where Self: UIViewController { 15 | 16 | func bindViewModel(to model: Self.ViewModelType) { 17 | viewModel = model 18 | loadViewIfNeeded() 19 | bindViewModel() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Architecture/ErrorTracker.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | class ErrorTracker: SharedSequenceConvertibleType { 6 | typealias SharingStrategy = DriverSharingStrategy 7 | private let _subject = PublishSubject() 8 | 9 | public init() { 10 | 11 | } 12 | 13 | func trackError(from source: O) -> Observable { 14 | return source.asObservable().do(onError: onError) 15 | } 16 | 17 | func asSharedSequence() -> SharedSequence { 18 | return _subject.asObservable().asDriverOnErrorJustComplete() 19 | } 20 | 21 | func asObservable() -> Observable { 22 | return _subject.asObservable() 23 | } 24 | 25 | private func onError(_ error: Error) { 26 | _subject.onNext(error) 27 | } 28 | 29 | deinit { 30 | _subject.onCompleted() 31 | } 32 | } 33 | 34 | extension ObservableConvertibleType { 35 | func trackError(_ errorTracker: ErrorTracker) -> Observable { 36 | return errorTracker.trackError(from: self) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Architecture/MultiActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | class MultiActivityIndicator: ActivityIndicator { 6 | private let _lock = NSRecursiveLock() 7 | private let _set = BehaviorRelay>(value: []) 8 | private let _loading: SharedSequence 9 | 10 | override init() { 11 | _loading = _set 12 | .asDriver() 13 | .map { !$0.isEmpty } 14 | .distinctUntilChanged() 15 | } 16 | 17 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 18 | let id = UUID().uuidString 19 | 20 | return source.asObservable() 21 | .do(onNext: { [weak self] _ in 22 | self?.sendStopLoading(id: id) 23 | }, onError: { [weak self] _ in 24 | self?.sendStopLoading(id: id) 25 | }, onCompleted: { [weak self] in 26 | self?.sendStopLoading(id: id) 27 | }, onSubscribe: { [weak self] in 28 | self?.subscribed(id: id) 29 | }) 30 | } 31 | 32 | private func subscribed(id: String) { 33 | _lock.lock() 34 | var set = _set.value 35 | set.insert(id) 36 | _set.accept(set) 37 | _lock.unlock() 38 | } 39 | 40 | private func sendStopLoading(id: String) { 41 | _lock.lock() 42 | var set = _set.value 43 | set.remove(id) 44 | _set.accept(set) 45 | _lock.unlock() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Architecture/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | protocol ViewModelType { 6 | associatedtype Input 7 | associatedtype Output 8 | 9 | func transform(_ input: Input, disposeBag: DisposeBag) -> Output 10 | } 11 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Extensions/Driver+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | extension SharedSequenceConvertibleType { 6 | 7 | func mapToVoid() -> SharedSequence { 8 | return map { _ in } 9 | } 10 | 11 | func mapToOptional() -> SharedSequence { 12 | return map { value -> Element? in value } 13 | } 14 | 15 | func unwrap() -> SharedSequence where Element == T? { 16 | return flatMap { SharedSequence.from(optional: $0) } 17 | } 18 | } 19 | 20 | extension SharedSequenceConvertibleType where Element == Bool { 21 | func not() -> SharedSequence { 22 | return map(!) 23 | } 24 | 25 | static func or(_ sources: SharedSequence...) 26 | -> SharedSequence { 27 | return Driver.combineLatest(sources) 28 | .map { $0.reduce(false) { $0 || $1 } } 29 | } 30 | 31 | static func and(_ sources: SharedSequence...) 32 | -> SharedSequence { 33 | return Driver.combineLatest(sources) 34 | .map { $0.reduce(true) { $0 && $1 } } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Extensions/ObservableType+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | extension ObservableType { 6 | 7 | func catchErrorJustComplete() -> Observable { 8 | return `catch` { _ in 9 | return Observable.empty() 10 | } 11 | } 12 | 13 | func asDriverOnErrorJustComplete() -> Driver { 14 | return asDriver { _ in 15 | return Driver.empty() 16 | } 17 | } 18 | 19 | func mapToVoid() -> Observable { 20 | return map { _ in } 21 | } 22 | 23 | func mapToOptional() -> Observable { 24 | return map { value -> Element? in value } 25 | } 26 | 27 | func unwrap() -> Observable where Element == T? { 28 | return flatMap { Observable.from(optional: $0) } 29 | } 30 | } 31 | 32 | extension ObservableType where Element == Bool { 33 | func not() -> Observable { 34 | return map(!) 35 | } 36 | 37 | static func or(_ sources: Observable...) 38 | -> Observable { 39 | return Observable.combineLatest(sources) 40 | .map { $0.reduce(false) { $0 || $1 } } 41 | } 42 | 43 | static func and(_ sources: Observable...) 44 | -> Observable { 45 | return Observable.combineLatest(sources) 46 | .map { $0.reduce(true) { $0 && $1 } } 47 | } 48 | } 49 | 50 | private func getThreadName() -> String { 51 | if Thread.current.isMainThread { 52 | return "Main Thread" 53 | } else if let name = Thread.current.name { 54 | if name.isEmpty { 55 | return "Anonymous Thread" 56 | } 57 | return name 58 | } else { 59 | return "Unknown Thread" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Extensions/UIViewController+Debug.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | extension UIViewController { 6 | func logDeinit() { 7 | print(String(describing: type(of: self)) + " deinit") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Models/GithubOwner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubOwner.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by Giang Dong Trinh on 08/10/2023. 6 | // Copyright © 2023 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Then 11 | 12 | struct GithubOwner: Codable { 13 | let id: Int 14 | let avatarURL: String 15 | let url: String 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case id = "id" 19 | case avatarURL = "avatar_url" 20 | case url = "url" 21 | } 22 | } 23 | 24 | extension GithubOwner: Then, Hashable { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Domain/Models/GithubRepo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepo.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Then 11 | 12 | struct GithubRepo: Codable { 13 | var id: Int 14 | var name: String 15 | var fullname: String 16 | var urlString: String 17 | var starCount: Int 18 | var folkCount: Int 19 | var owner: GithubOwner 20 | 21 | enum CodingKeys: String, CodingKey { 22 | case id = "id" 23 | case name = "name" 24 | case fullname = "full_name" 25 | case urlString = "html_url" 26 | case starCount = "stargazers_count" 27 | case folkCount = "forks" 28 | case owner = "owner" 29 | } 30 | } 31 | 32 | extension GithubRepo: Then, Hashable { 33 | 34 | } 35 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Extensions/Array+.swift: -------------------------------------------------------------------------------- 1 | extension Array { 2 | public var isNotEmpty: Bool { 3 | return !self.isEmpty 4 | } 5 | 6 | subscript (safe index: Index) -> Iterator.Element? { 7 | return indices.contains(index) ? self[index] : nil 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Extensions/UIViewController+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIViewController { 13 | 14 | func showError(message: String, completion: (() -> Void)? = nil) { 15 | let ac = UIAlertController(title: "Error", 16 | message: message, 17 | preferredStyle: .alert) 18 | let okAction = UIAlertAction(title: "OK", style: .cancel) { _ in 19 | completion?() 20 | } 21 | ac.addAction(okAction) 22 | present(ac, animated: true, completion: nil) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Extensions/ViewController+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | import MBProgressHUD 13 | 14 | extension Reactive where Base: UIViewController { 15 | 16 | var error: Binder { 17 | return Binder(base) { viewController, error in 18 | viewController.showError(message: error.localizedDescription) 19 | } 20 | } 21 | 22 | var isLoading: Binder { 23 | return Binder(base) { viewController, isLoading in 24 | if isLoading { 25 | let hud = MBProgressHUD.showAdded(to: viewController.view, animated: true) 26 | hud.offset.y = -30 27 | } else { 28 | MBProgressHUD.hide(for: viewController.view, animated: true) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Simple Demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Platform/Repositories/GithubRepoRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepoRepository.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | protocol GithubRepoRepositoryType { 13 | func getGithubRepos(input request: GithubRepoRequest) -> Observable<[GithubRepo]> 14 | } 15 | 16 | class GithubRepoRepository: GithubRepoRepositoryType { 17 | private let apiService: APIService = APIService.shared 18 | 19 | func getGithubRepos(input request: GithubRepoRequest) -> Observable<[GithubRepo]> { 20 | return apiService.request(.repositories(request: request)) 21 | .map(GithubRepoResponse.self) 22 | .asObservable() 23 | .map { 24 | $0.githubRepos 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Platform/Services/API/APIService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIService.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by Giang Dong Trinh on 08/10/2023. 6 | // Copyright © 2023 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | import Moya 13 | import Alamofire 14 | 15 | protocol APIServiceType { 16 | func request(_ target: APITarget) -> Single 17 | } 18 | 19 | class APIService: APIServiceType { 20 | 21 | static let shared = APIService() 22 | 23 | private let provider: MoyaProvider 24 | private let semaphore = DispatchSemaphore(value: 1) 25 | 26 | private init() { 27 | let configuration = URLSessionConfiguration.default.with { 28 | $0.headers = .default 29 | $0.timeoutIntervalForRequest = 30 30 | $0.timeoutIntervalForResource = 60 31 | } 32 | 33 | let session = Alamofire.Session(configuration: configuration) 34 | 35 | let networkPlugin = NetworkLoggerPlugin(configuration: NetworkLoggerPlugin.Configuration(logOptions: .verbose)) 36 | 37 | provider = MoyaProvider(session: session, plugins: [networkPlugin]) 38 | } 39 | 40 | // MARK: - Functions 41 | func request(_ target: APITarget) -> Single { 42 | let request = Single.just(()) 43 | .do(onSuccess: { response in 44 | print("[LOG 🌏][\(target.method.rawValue)] Request \(target.baseURL.absoluteString + target.path)") 45 | }) 46 | .flatMap { [unowned self] _ -> Single in 47 | return self.provider.rx.request(target) 48 | } 49 | .catchApiError() 50 | return request 51 | } 52 | } 53 | 54 | extension PrimitiveSequence where Trait == SingleTrait, Element == Response { 55 | 56 | func catchApiError() -> Single { 57 | let response = flatMap { response -> Single in 58 | switch response.statusCode { 59 | case 200...299: 60 | print("[LOG ✅] Request Success \(response.request?.url?.absoluteString ?? "")") 61 | return .just(response) 62 | default: 63 | print("[LOG ❌] Request Error \(response.statusCode) \(response.request?.url?.absoluteString ?? "")") 64 | let domain = response.request?.url?.absoluteString ?? "" 65 | let code = response.statusCode 66 | let errorMessage = HTTPURLResponse.localizedString(forStatusCode: response.statusCode) 67 | throw NSError(domain: domain, 68 | code: code, 69 | userInfo: [NSLocalizedDescriptionKey: errorMessage]) 70 | } 71 | } 72 | return response 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Platform/Services/API/APITarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APITarget.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by Giang Dong Trinh on 08/10/2023. 6 | // Copyright © 2023 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | enum APITarget { 13 | case repositories(request: GithubRepoRequest) 14 | } 15 | 16 | extension APITarget: TargetType { 17 | 18 | var baseURL: URL { 19 | let domain = URLs.API.baseUrl 20 | guard let url = URL(string: domain) else { 21 | fatalError("Invalid base URL.") 22 | } 23 | return url 24 | } 25 | 26 | var headers: [String : String]? { 27 | switch self { 28 | case .repositories: 29 | return [ 30 | "Content-type": "application/json", 31 | "Accept": "application/json" 32 | ] 33 | } 34 | } 35 | 36 | var path: String { 37 | switch self { 38 | case .repositories: 39 | return "/search/repositories" 40 | } 41 | } 42 | 43 | var method: Moya.Method { 44 | switch self { 45 | case .repositories: 46 | return .get 47 | } 48 | } 49 | 50 | var task: Task { 51 | switch self { 52 | case .repositories(let request): 53 | let parameters: [String: Any] = [ 54 | "q": request.language, 55 | "per_page": request.perPage, 56 | "page": request.page 57 | ] 58 | return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) 59 | } 60 | } 61 | 62 | var sampleData: Data { 63 | // Bundle.main.loadData(fileName: "") ?? Data() 64 | return Data() 65 | } 66 | } 67 | 68 | extension String { 69 | /// Throw crash error if API did not define in documentation. 70 | static var throwDocumentError: String { 71 | fatalError("API document did not define!") 72 | } 73 | } 74 | 75 | extension Moya.Method { 76 | /// Throw crash error if API did not define in documentation. 77 | static var throwDocumentError: Moya.Method { 78 | fatalError("API document did not define!") 79 | } 80 | } 81 | 82 | extension Task { 83 | /// Throw crash error if API did not define in documentation. 84 | static var throwDocumentError: Task { 85 | fatalError("API document did not define!") 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Platform/Services/API/Request/GithubRepoRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepoRequest.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | struct GithubRepoRequest { 13 | let page: Int 14 | let perPage: Int 15 | let language: String 16 | 17 | init(page: Int, perPage: Int = 10, language: String = "language:swift") { 18 | self.page = page 19 | self.perPage = perPage 20 | self.language = language 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Platform/Services/API/Response/GithubRepoResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepoResponse.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct GithubRepoResponse: Codable { 12 | let githubRepos: [GithubRepo] 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case githubRepos = "items" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/App/AppNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppNavigator.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol AppNavigatorType { 14 | func toMain() 15 | } 16 | 17 | struct AppNavigator: AppNavigatorType { 18 | unowned let window: UIWindow 19 | 20 | func toMain() { 21 | let viewController = MainViewController.instantiate() 22 | let navigationController = UINavigationController(rootViewController: viewController) 23 | let navigator = MainNavigator(navigationController: navigationController) 24 | let useCase = MainUseCase() 25 | let viewModel = MainViewModel(navigator: navigator, useCase: useCase) 26 | 27 | viewController.bindViewModel(to: viewModel) 28 | window.rootViewController = navigationController 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/App/AppUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppUseCase.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol AppUseCaseType { 14 | 15 | } 16 | 17 | struct AppUseCase: AppUseCaseType { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/App/AppViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppViewModel.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | struct AppViewModel { 14 | let navigator: AppNavigatorType 15 | let useCase: AppUseCaseType 16 | } 17 | 18 | extension AppViewModel: ViewModelType { 19 | struct Input { 20 | let loadTrigger: Driver 21 | } 22 | 23 | struct Output { 24 | 25 | } 26 | 27 | func transform(_ input: AppViewModel.Input, disposeBag: DisposeBag) -> AppViewModel.Output { 28 | 29 | input.loadTrigger 30 | .do(onNext: { _ in 31 | self.navigator.toMain() 32 | }) 33 | .drive() 34 | .disposed(by: disposeBag) 35 | 36 | return Output() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/Main/Cell/GithubRepoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubRepoCell.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SDWebImage 11 | import Reusable 12 | 13 | class GithubRepoCell: UITableViewCell, NibReusable { 14 | @IBOutlet weak var avatarImageView: UIImageView! 15 | @IBOutlet weak var nameLabel: UILabel! 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | } 20 | 21 | func setContentForCell(_ githubRepo: GithubRepo) { 22 | avatarImageView.sd_setImage(with: URL(string: githubRepo.owner.avatarURL), completed: nil) 23 | nameLabel.text = githubRepo.name 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/Main/Cell/GithubRepoCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/Main/MainNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainNavigator.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxSwift 12 | import RxCocoa 13 | 14 | protocol MainNavigatorType { 15 | func toRepoDetail(githubRepo: GithubRepo) 16 | } 17 | 18 | struct MainNavigator: MainNavigatorType { 19 | unowned let navigationController: UINavigationController 20 | 21 | func toRepoDetail(githubRepo: GithubRepo) { 22 | let viewController = RepoDetailViewController.instantiate() 23 | let useCase = RepoDetailUseCase() 24 | let navigator = RepoDetailNavigator(navigationController: navigationController) 25 | let viewModel = RepoDetailViewModel(navigator: navigator, 26 | useCase: useCase, 27 | repo: githubRepo) 28 | viewController.bindViewModel(to: viewModel) 29 | navigationController.pushViewController(viewController, animated: true) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/Main/MainUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainUseCase.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol MainUseCaseType { 14 | func getRepos() -> Observable<[GithubRepo]> 15 | } 16 | 17 | struct MainUseCase: MainUseCaseType { 18 | 19 | func getRepos() -> Observable<[GithubRepo]> { 20 | let request = GithubRepoRequest(page: 1, perPage: 20) 21 | let repository = GithubRepoRepository() 22 | return repository.getGithubRepos(input: request) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/Main/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | import RxSwift 12 | import RxCocoa 13 | import Then 14 | import NSObject_Rx 15 | import Reusable 16 | 17 | class MainViewController: UIViewController, BindableType { 18 | @IBOutlet weak var tableView: UITableView! 19 | 20 | var disposeBag: DisposeBag! = DisposeBag() 21 | var viewModel: MainViewModel! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | configView() 27 | } 28 | 29 | private func configView() { 30 | title = "Github" 31 | tableView.do { 32 | $0.register(cellType: GithubRepoCell.self) 33 | $0.rowHeight = 80 34 | } 35 | } 36 | 37 | func bindViewModel() { 38 | let input = MainViewModel.Input( 39 | loadTrigger: Driver.just(()), 40 | selectTrigger: tableView.rx.itemSelected.asDriver() 41 | ) 42 | let output = viewModel.transform(input, disposeBag: disposeBag) 43 | 44 | output.repos 45 | .drive(tableView.rx.items) { tableView, index, repo in 46 | let indexPath = IndexPath(item: index, section: 0) 47 | let cell: GithubRepoCell = tableView.dequeueReusableCell(for: indexPath) 48 | cell.setContentForCell(repo) 49 | return cell 50 | } 51 | .disposed(by: rx.disposeBag) 52 | 53 | output.indicator 54 | .drive(rx.isLoading) 55 | .disposed(by: rx.disposeBag) 56 | 57 | output.error 58 | .drive(rx.error) 59 | .disposed(by: rx.disposeBag) 60 | } 61 | } 62 | 63 | extension MainViewController: StoryboardSceneBased { 64 | static var sceneStoryboard = StoryBoards.main 65 | } 66 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/Main/MainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewModel.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | struct MainViewModel { 14 | let navigator: MainNavigatorType 15 | let useCase: MainUseCaseType 16 | } 17 | 18 | extension MainViewModel: ViewModelType { 19 | struct Input { 20 | let loadTrigger: Driver 21 | let selectTrigger: Driver 22 | } 23 | 24 | struct Output { 25 | let repos: Driver<[GithubRepo]> 26 | let error: Driver 27 | let indicator: Driver 28 | } 29 | 30 | func transform(_ input: MainViewModel.Input, disposeBag: DisposeBag) -> MainViewModel.Output { 31 | let indicator = ActivityIndicator() 32 | let error = ErrorTracker() 33 | 34 | let repos = input.loadTrigger 35 | .flatMapLatest { _ in 36 | return self.useCase.getRepos() 37 | .trackActivity(indicator) 38 | .trackError(error) 39 | .asDriverOnErrorJustComplete() 40 | } 41 | 42 | input.selectTrigger 43 | .withLatestFrom(repos) { indexPath, repos in 44 | return repos[indexPath.row] 45 | } 46 | .do(onNext: { repo in 47 | self.navigator.toRepoDetail(githubRepo: repo) 48 | }) 49 | .drive() 50 | .disposed(by: disposeBag) 51 | 52 | return Output( 53 | repos: repos, 54 | error: error.asDriver(), 55 | indicator: indicator.asDriver() 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/RepoDetail/RepoDetailNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoDetailNavigator.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxSwift 12 | import RxCocoa 13 | 14 | protocol RepoDetailNavigatorType { 15 | 16 | } 17 | 18 | struct RepoDetailNavigator: RepoDetailNavigatorType { 19 | unowned let navigationController: UINavigationController 20 | } 21 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/RepoDetail/RepoDetailUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoDetailUseCase.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol RepoDetailUseCaseType { 14 | 15 | } 16 | 17 | struct RepoDetailUseCase: RepoDetailUseCaseType { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/RepoDetail/RepoDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoDetailViewController.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | import RxSwift 12 | import RxCocoa 13 | import Then 14 | import NSObject_Rx 15 | import Reusable 16 | import SDWebImage 17 | 18 | class RepoDetailViewController: UIViewController, BindableType { 19 | @IBOutlet weak var avatarImageView: UIImageView! 20 | @IBOutlet weak var nameLabel: UILabel! 21 | 22 | var disposeBag: DisposeBag! = DisposeBag() 23 | var viewModel: RepoDetailViewModel! 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | configView() 29 | } 30 | 31 | private func configView() { 32 | title = "Gitgub Detail" 33 | } 34 | 35 | func bindViewModel() { 36 | let input = RepoDetailViewModel.Input( 37 | loadTrigger: Driver.just(()) 38 | ) 39 | let output = viewModel.transform(input, disposeBag: disposeBag) 40 | 41 | output.repoImage 42 | .drive(avatarBinding) 43 | .disposed(by: rx.disposeBag) 44 | 45 | output.repoName 46 | .drive(nameLabel.rx.text) 47 | .disposed(by: rx.disposeBag) 48 | 49 | output.indicator 50 | .drive(rx.isLoading) 51 | .disposed(by: rx.disposeBag) 52 | 53 | output.error 54 | .drive(rx.error) 55 | .disposed(by: rx.disposeBag) 56 | } 57 | } 58 | 59 | extension RepoDetailViewController { 60 | 61 | var avatarBinding: Binder { 62 | return Binder(self) { viewController, imageUrl in 63 | let url = URL(string: imageUrl) 64 | viewController.avatarImageView.sd_setImage(with: url, completed: nil) 65 | } 66 | } 67 | } 68 | 69 | extension RepoDetailViewController: StoryboardSceneBased { 70 | static var sceneStoryboard = StoryBoards.main 71 | } 72 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/RepoDetail/RepoDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoDetailViewModel.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | struct RepoDetailViewModel { 14 | let navigator: RepoDetailNavigatorType 15 | let useCase: RepoDetailUseCaseType 16 | let repo: GithubRepo 17 | } 18 | 19 | extension RepoDetailViewModel: ViewModelType { 20 | struct Input { 21 | let loadTrigger: Driver 22 | } 23 | 24 | struct Output { 25 | let repoName: Driver 26 | let repoImage: Driver 27 | let error: Driver 28 | let indicator: Driver 29 | } 30 | 31 | func transform(_ input: RepoDetailViewModel.Input, disposeBag: DisposeBag) -> RepoDetailViewModel.Output { 32 | let indicator = ActivityIndicator() 33 | let error = ErrorTracker() 34 | 35 | let repoName = input.loadTrigger 36 | .map { _ in 37 | return self.repo.name 38 | } 39 | 40 | let repoImageUrl = input.loadTrigger 41 | .map { _ in 42 | return self.repo.owner.avatarURL 43 | } 44 | 45 | return Output( 46 | repoName: repoName, 47 | repoImage: repoImageUrl, 48 | error: error.asDriver(), 49 | indicator: indicator.asDriver() 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/StoryBoards/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Scenes/StoryBoards/StoryBoards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryBoards.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | struct StoryBoards { 13 | static let main = UIStoryboard(name: "Main", bundle: nil) 14 | } 15 | -------------------------------------------------------------------------------- /SimpleDemoCleanArchitecture/Utils/URLs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLs.swift 3 | // SimpleDemoCleanArchitecture 4 | // 5 | // Created by trinh.giang.dong on 2/22/19. 6 | // Copyright © 2019 trinh.giang.dong. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct URLs { 12 | enum API { 13 | #if PRD 14 | static let baseUrl = "https://api.github.com" 15 | #else 16 | static let baseUrl = "https://api.github.com" 17 | #endif 18 | } 19 | } 20 | --------------------------------------------------------------------------------