├── .gitignore ├── LICENSE ├── README.md ├── Reactor ├── Podfile ├── Podfile.lock ├── Reactor.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Reactor.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Reactor │ ├── App │ ├── AppDelegate.swift │ ├── Models │ │ └── Repo.swift │ ├── Networking │ │ └── NetworkingApi.swift │ ├── RepoScene │ │ ├── RepoViewModel.swift │ │ ├── ReposReactor.swift │ │ └── ReposViewController.swift │ └── Utils │ │ └── UIViewController+Rx.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ └── Info.plist ├── app-ui.gif ├── multiscreen-ui.gif ├── mvc ├── MVC.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── MVC.xcscheme ├── MVC │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── Utils │ │ │ │ ├── CancellableReposFetcher.swift │ │ │ │ ├── ReposDataSource.swift │ │ │ │ └── ThrottledTextFieldValidator.swift │ │ └── Utils │ │ │ └── Throttle.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── MVVMDelegates │ │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── ReposViewModel.swift │ │ └── Utils │ │ │ └── Throttle.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ └── Info.plist └── README.md ├── mvp ├── MVP.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVP │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── ReposViewModel.swift │ │ └── Utils │ │ │ └── Throttle.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── MVVMDelegates │ │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── ReposViewModel.swift │ │ └── Utils │ │ │ └── Throttle.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ └── Info.plist ├── README.md └── example-scenario.png ├── mvvm-closures ├── MVVMClosures.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMClosures │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── ReposViewModel.swift │ │ └── Utils │ │ │ └── Throttle.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── README.md └── example-scenario.png ├── mvvm-functions-subjects-observables ├── MVVMFunctionsSubjectsObservables.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMFunctionsSubjectsObservables.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMFunctionsSubjectsObservables │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Environments │ │ │ └── AppEnvironment.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── ReposViewModel.swift │ │ └── Utils │ │ │ ├── ActivityIndicator.swift │ │ │ ├── UIViewController+Rx.swift │ │ │ └── ViewModelType.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Podfile ├── Podfile.lock ├── README.md └── example-scenario.png ├── mvvm-rxswift-pure ├── MVVMPureObservables.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMPureObservables.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMPureObservables │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── ReposViewModel.swift │ │ └── Utils │ │ │ ├── ActivityIndicator.swift │ │ │ ├── UIViewController+Rx.swift │ │ │ └── ViewModelType.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Podfile ├── Podfile.lock ├── README.md └── example-scenario.png ├── mvvm-rxswift-subjects-observables ├── MVVMFunctionsSubjectsObservables.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMSubjectsObservables.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMSubjectsObservables.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MVVMSubjectsObservables │ ├── App │ │ ├── AppDelegate.swift │ │ ├── Models │ │ │ └── Repo.swift │ │ ├── Networking │ │ │ └── NetworkingApi.swift │ │ ├── ReposScene │ │ │ ├── ReposViewController.swift │ │ │ └── ReposViewModel.swift │ │ └── Utils │ │ │ ├── ActivityIndicator.swift │ │ │ ├── UIViewController+Rx.swift │ │ │ └── ViewModelType.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Podfile ├── Podfile.lock ├── README.md └── example-scenario.png ├── rxfeedback-mvc ├── Podfile ├── Podfile.lock ├── README.md ├── rxfeedback-mvc.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── rxfeedback-mvc.xcscheme ├── rxfeedback-mvc.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── rxfeedback-mvc │ ├── App │ ├── AppDelegate.swift │ ├── Models │ │ └── Repo.swift │ ├── Networking │ │ └── NetworkingApi.swift │ └── ReposScene │ │ └── ReposViewController.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ └── Info.plist ├── tmdb-mvvm-rxswift-pure ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── Podfile ├── Podfile.lock ├── README.md ├── tmdb-mvvm-pure.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── tmdb-mvvm-pure.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── tmdb-mvvm-pure │ ├── App │ │ ├── Application │ │ │ ├── App.swift │ │ │ └── AppDelegate.swift │ │ ├── Models │ │ │ ├── Genre.swift │ │ │ ├── Movie.swift │ │ │ ├── Person.swift │ │ │ └── Show.swift │ │ ├── Networking │ │ │ ├── HTTPClient.swift │ │ │ ├── TMDBApi.swift │ │ │ └── TMDBApiResponses.swift │ │ ├── Scenes │ │ │ ├── Discover │ │ │ │ ├── DiscoverNavigator.swift │ │ │ │ ├── DiscoverViewController.swift │ │ │ │ ├── DiscoverViewModel.swift │ │ │ │ └── Views │ │ │ │ │ ├── CarouselSectionCell.swift │ │ │ │ │ ├── CarouselSectionCell.xib │ │ │ │ │ ├── DiscoverMainView.swift │ │ │ │ │ ├── DiscoverMainView.xib │ │ │ │ │ ├── MovieCell.swift │ │ │ │ │ └── MovieCell.xib │ │ │ ├── Login │ │ │ │ ├── LoginNavigator.swift │ │ │ │ ├── LoginViewController.swift │ │ │ │ └── LoginViewModel.swift │ │ │ ├── MovieDetail │ │ │ │ ├── MovieDetailNavigator.swift │ │ │ │ ├── MovieDetailViewController.swift │ │ │ │ ├── MovieDetailViewModel.swift │ │ │ │ └── Views │ │ │ │ │ ├── GradientImageView.swift │ │ │ │ │ ├── MovieDetailHeaderView.swift │ │ │ │ │ ├── MovieDetailHeaderView.xib │ │ │ │ │ ├── MovieDetailTipsView.swift │ │ │ │ │ └── MovieDetailTipsView.xib │ │ │ └── Search │ │ │ │ ├── SearchNavigator.swift │ │ │ │ ├── SearchViewController.swift │ │ │ │ ├── SearchViewModel.swift │ │ │ │ └── Views │ │ │ │ ├── SearchCell.swift │ │ │ │ └── SearchCell.xib │ │ ├── Storyboard │ │ │ └── Base.lproj │ │ │ │ └── Main.storyboard │ │ └── Utils │ │ │ ├── ActivityIndicator.swift │ │ │ ├── UIStoryboard+ViewControllers.swift │ │ │ ├── UIViewController+Rx.swift │ │ │ └── ViewModelType.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── aquaman.imageset │ │ │ ├── Contents.json │ │ │ └── aquaman.jpg │ │ ├── back_arrow.imageset │ │ │ ├── Contents.json │ │ │ └── icons8-left-100 (1).png │ │ ├── sample.imageset │ │ │ ├── Contents.json │ │ │ └── sample.jpg │ │ ├── search_icon.imageset │ │ │ ├── Contents.json │ │ │ └── icons8-search-100.png │ │ └── star_icon.imageset │ │ │ ├── Contents.json │ │ │ └── icons8-star-filled-96.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist └── tmdb-mvvm-pureTests │ ├── Info.plist │ └── tmdb_mvvm_pureTests.swift └── viper ├── README.md ├── VIPER.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── VIPER.xcscheme └── VIPER ├── App ├── AppDelegate.swift ├── Entities │ └── Repo.swift ├── Modules │ └── Repos │ │ ├── CancellableReposFetcher.swift │ │ ├── ReposInteractor.swift │ │ ├── ReposPresenter.swift │ │ ├── ReposProtocols.swift │ │ ├── ReposRemoteDataManager.swift │ │ ├── ReposViewController.swift │ │ ├── ReposWireframe.swift │ │ └── ThrottledTextFieldValidator.swift ├── Networking │ └── NetworkingApi.swift ├── ReposScene │ ├── ReposViewController111.swift │ └── ReposViewModel.swift └── Utils │ └── Throttle.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X Finder 2 | **/.DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | 19 | ## Other 20 | *.xccheckout 21 | *.moved-aside 22 | *.xcuserstate 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | # CocoaPods 30 | **/Pods 31 | Pods/ 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pawel Krawiec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Reactor/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Reactor' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | pod 'ReactorKit' 8 | pod 'RxSwift' 9 | pod 'RxCocoa' 10 | 11 | # Pods for Reactor 12 | 13 | end 14 | -------------------------------------------------------------------------------- /Reactor/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - ReactorKit (1.2.1): 3 | - RxSwift (>= 4.0.0) 4 | - RxAtomic (4.4.2) 5 | - RxCocoa (4.4.2): 6 | - RxSwift (>= 4.4.2, ~> 4.4) 7 | - RxSwift (4.4.2): 8 | - RxAtomic (>= 4.4.2, ~> 4.4) 9 | 10 | DEPENDENCIES: 11 | - ReactorKit 12 | - RxCocoa 13 | - RxSwift 14 | 15 | SPEC REPOS: 16 | https://github.com/cocoapods/specs.git: 17 | - ReactorKit 18 | - RxAtomic 19 | - RxCocoa 20 | - RxSwift 21 | 22 | SPEC CHECKSUMS: 23 | ReactorKit: 8be57f0527ad1ac2963cd5c338274a46a01e57a3 24 | RxAtomic: d00e97c10db88c6f08540e0bf2752fc5a2404167 25 | RxCocoa: 477990dc3b4c3ff55fb0ac77e1cc06244e0aaec8 26 | RxSwift: 74c29b693c8e42b0f64400e8b06564575742d649 27 | 28 | PODFILE CHECKSUM: fbb955e82cbcd8943e557b18dc8fbe1948b10896 29 | 30 | COCOAPODS: 1.5.3 31 | -------------------------------------------------------------------------------- /Reactor/Reactor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Reactor/Reactor.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Reactor/Reactor.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Reactor/Reactor.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Reactor/Reactor/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /Reactor/Reactor/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol NetworkingService { 14 | func searchRepos(withQuery query: String) -> Observable<[Repo]> 15 | } 16 | 17 | final class NetworkingApi: NetworkingService { 18 | func searchRepos(withQuery query: String) -> Observable<[Repo]> { 19 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 20 | return URLSession.shared.rx.data(request: request) 21 | .map { data -> [Repo] in 22 | guard let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { return [] } 23 | return response.items 24 | } 25 | } 26 | } 27 | 28 | fileprivate struct SearchResponse: Decodable { 29 | let items: [Repo] 30 | } 31 | -------------------------------------------------------------------------------- /Reactor/Reactor/App/RepoScene/RepoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoViewModel.swift 3 | // Reactor 4 | // 5 | // Created by KangJungu on 25/03/2019. 6 | // Copyright © 2019 June. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct RepoViewModel { 12 | let name: String 13 | var hashValue:Int 14 | } 15 | 16 | extension RepoViewModel: Hashable { 17 | init(repo: Repo) { 18 | self.name = repo.name 19 | self.hashValue = repo.id.hashValue 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reactor/Reactor/App/RepoScene/ReposReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposReactor.swift 3 | // Reactor 4 | // 5 | // Created by KangJungu on 25/03/2019. 6 | // Copyright © 2019 June. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import ReactorKit 11 | 12 | final class ReposReactor:Reactor { 13 | enum Action { 14 | case ready 15 | case search(text:String) 16 | case selected(index:IndexPath) 17 | } 18 | 19 | enum Mutation { 20 | case indicator(start:Bool) 21 | case selected(index:IndexPath) 22 | case searchEnded(repos:[Repo]) 23 | } 24 | 25 | struct State { 26 | var loading:Bool 27 | var respoViewModels:[RepoViewModel] {return respos.map{RepoViewModel(repo: $0)}} 28 | fileprivate var respos:[Repo] 29 | var selectedRepoId:Int? 30 | } 31 | 32 | struct Dependencies { 33 | let networking: NetworkingService 34 | } 35 | 36 | private let dependencies: Dependencies 37 | let initialState: ReposReactor.State 38 | 39 | init(dependencies: Dependencies) { 40 | self.dependencies = dependencies 41 | self.initialState = State(loading: false, respos: [], selectedRepoId: nil) 42 | } 43 | 44 | func mutate(action: ReposReactor.Action) -> Observable { 45 | switch action { 46 | case .ready: 47 | return self.search("swift") 48 | case .search(let text): 49 | guard text.count > 2 else {return Observable.empty()} 50 | return self.search(text) 51 | case .selected(let indexPath): 52 | return Observable.just(Mutation.selected(index: indexPath)) 53 | } 54 | } 55 | 56 | private func search(_ text:String) -> Observable { 57 | let searchObservable:Observable = self.dependencies.networking.searchRepos(withQuery: text).map{Mutation.searchEnded(repos: $0)} 58 | 59 | return Observable.concat( 60 | Observable.just(Mutation.indicator(start: true)), 61 | searchObservable, 62 | Observable.just(Mutation.indicator(start: false)) 63 | ) 64 | } 65 | 66 | func reduce(state: ReposReactor.State, mutation: ReposReactor.Mutation) -> ReposReactor.State { 67 | var state = state 68 | switch mutation { 69 | case .indicator(let start): 70 | state.loading = start 71 | case .searchEnded(let repos): 72 | state.respos = repos 73 | case .selected(let index): 74 | state.selectedRepoId = currentState.respos[index.item].id 75 | } 76 | return state 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Reactor/Reactor/App/Utils/UIViewController+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Rx.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | extension Reactive where Base: UIViewController { 13 | var viewWillAppear: ControlEvent { 14 | let source = self.methodInvoked(#selector(Base.viewWillAppear(_:))).map { _ in } 15 | return ControlEvent(events: source) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Reactor/Reactor/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Reactor/Reactor/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Reactor/Reactor/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 | -------------------------------------------------------------------------------- /Reactor/Reactor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app-ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/app-ui.gif -------------------------------------------------------------------------------- /multiscreen-ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/multiscreen-ui.gif -------------------------------------------------------------------------------- /mvc/MVC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mvc/MVC.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvc/MVC/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let viewController = ReposViewController() 21 | window.rootViewController = UINavigationController(rootViewController: viewController) 22 | window.makeKeyAndVisible() 23 | } 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mvc/MVC/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /mvc/MVC/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | protocol NetworkingService { 13 | @discardableResult func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask 14 | } 15 | 16 | final class NetworkingApi: NetworkingService { 17 | private let session = URLSession.shared 18 | 19 | @discardableResult 20 | func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask { 21 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 22 | let task = session.dataTask(with: request) { (data, _, _) in 23 | DispatchQueue.main.async { 24 | guard let data = data, 25 | let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { 26 | completion([]) 27 | return 28 | } 29 | completion(response.items) 30 | } 31 | } 32 | task.resume() 33 | return task 34 | } 35 | } 36 | 37 | fileprivate struct SearchResponse: Decodable { 38 | let items: [Repo] 39 | } 40 | -------------------------------------------------------------------------------- /mvc/MVC/App/ReposScene/Utils/CancellableReposFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CancellableReposFetcher.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 01/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class CancellableReposFetcher { 12 | private var currentSearchNetworkTask: URLSessionDataTask? 13 | private let networkingService: NetworkingService 14 | 15 | init(networkingService: NetworkingService = NetworkingApi()) { 16 | self.networkingService = networkingService 17 | } 18 | 19 | func fetchRepos(withQuery query: String, completion: @escaping (([Repo]) -> ())) { 20 | currentSearchNetworkTask?.cancel() // cancel previous pending request 21 | 22 | _ = currentSearchNetworkTask = networkingService.searchRepos(withQuery: query) { repos in 23 | completion(repos) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mvc/MVC/App/ReposScene/Utils/ReposDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposDataSource.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 01/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class ReposDataSource: NSObject, UITableViewDataSource { 12 | var data: [Repo]? 13 | 14 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 15 | return data?.count ?? 0 16 | } 17 | 18 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 19 | guard let data = data else { return UITableViewCell() } 20 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 21 | cell.textLabel?.text = data[indexPath.row].name 22 | return cell 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /mvc/MVC/App/ReposScene/Utils/ThrottledTextFieldValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThrottledTextFieldValidator.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 01/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class ThrottledTextFieldValidator { 12 | private var lastQuery: String? 13 | private let throttle: Throttle 14 | private let validationRule: ((String) -> Bool) 15 | 16 | init(throttle: Throttle = Throttle(minimumDelay: 0.3), 17 | validationRule: @escaping ((String) -> Bool) = { query in return query.count > 2 }) { 18 | self.throttle = throttle 19 | self.validationRule = validationRule 20 | } 21 | 22 | func validate(query: String, 23 | completion: @escaping ((String?) -> ())) { 24 | guard validationRule(query), 25 | distinctUntilChanged(query) else { 26 | completion(nil) 27 | return 28 | } 29 | throttle.throttle { 30 | completion(query) 31 | } 32 | } 33 | 34 | private func distinctUntilChanged(_ query: String) -> Bool { 35 | let valid = lastQuery != query 36 | lastQuery = query 37 | return valid 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mvc/MVC/App/Utils/Throttle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Throttle.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Throttle { 12 | private var workItem: DispatchWorkItem = DispatchWorkItem(block: {}) 13 | private var previousRun: Date = Date.distantPast 14 | private let queue: DispatchQueue 15 | private let delay: TimeInterval 16 | 17 | init(minimumDelay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) { 18 | self.delay = minimumDelay 19 | self.queue = queue 20 | } 21 | 22 | func throttle(_ block: @escaping () -> Void) { 23 | workItem.cancel() 24 | 25 | workItem = DispatchWorkItem() { 26 | [weak self] in 27 | self?.previousRun = Date() 28 | block() 29 | } 30 | 31 | let deltaDelay = previousRun.timeIntervalSinceNow > delay ? 0 : delay 32 | queue.asyncAfter(deadline: .now() + Double(deltaDelay), execute: workItem) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mvc/MVC/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvc/MVC/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvc/MVC/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 | -------------------------------------------------------------------------------- /mvc/MVC/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/App/ReposScene/ReposViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposViewController.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/App/ReposScene/ReposViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposViewModel.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/App/Utils/Throttle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Throttle.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/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 | -------------------------------------------------------------------------------- /mvc/MVC/MVVMDelegates/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /mvc/README.md: -------------------------------------------------------------------------------- 1 | # mvc 2 | This example uses standard iOS MVC pattern. 3 | 4 | 5 | ## Implementation 6 | In progress. 7 | 8 | ## Installation 9 | Clone the repository: 10 | 11 | `git clone git@github.com:tailec/ios-architecture.git` 12 | 13 | Navigate to `mvc` directory: 14 | 15 | `cd mvc` 16 | 17 | No `pod install` is required in this example. 18 | -------------------------------------------------------------------------------- /mvp/MVP.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mvp/MVP.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvp/MVP/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let viewModel = ReposViewModel(networkingService: NetworkingApi()) 21 | let viewController = ReposViewController(viewModel: viewModel) 22 | 23 | window.rootViewController = UINavigationController(rootViewController: viewController) 24 | window.makeKeyAndVisible() 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mvp/MVP/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /mvp/MVP/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | protocol NetworkingService { 13 | @discardableResult func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask 14 | } 15 | 16 | final class NetworkingApi: NetworkingService { 17 | private let session = URLSession.shared 18 | 19 | @discardableResult 20 | func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask { 21 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 22 | let task = session.dataTask(with: request) { (data, _, _) in 23 | DispatchQueue.main.async { 24 | guard let data = data, 25 | let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { 26 | completion([]) 27 | return 28 | } 29 | completion(response.items) 30 | } 31 | } 32 | task.resume() 33 | return task 34 | } 35 | } 36 | 37 | fileprivate struct SearchResponse: Decodable { 38 | let items: [Repo] 39 | } 40 | -------------------------------------------------------------------------------- /mvp/MVP/App/Utils/Throttle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Throttle.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Throttle { 12 | private var workItem: DispatchWorkItem = DispatchWorkItem(block: {}) 13 | private var previousRun: Date = Date.distantPast 14 | private let queue: DispatchQueue 15 | private let delay: TimeInterval 16 | 17 | init(minimumDelay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) { 18 | self.delay = minimumDelay 19 | self.queue = queue 20 | } 21 | 22 | func throttle(_ block: @escaping () -> Void) { 23 | workItem.cancel() 24 | 25 | workItem = DispatchWorkItem() { 26 | [weak self] in 27 | self?.previousRun = Date() 28 | block() 29 | } 30 | 31 | let deltaDelay = previousRun.timeIntervalSinceNow > delay ? 0 : delay 32 | queue.asyncAfter(deadline: .now() + Double(deltaDelay), execute: workItem) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mvp/MVP/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvp/MVP/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvp/MVP/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 | -------------------------------------------------------------------------------- /mvp/MVP/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/App/ReposScene/ReposViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposViewController.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/App/ReposScene/ReposViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposViewModel.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/App/Utils/Throttle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Throttle.swift 3 | // MVVMDelegates 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/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 | -------------------------------------------------------------------------------- /mvp/MVP/MVVMDelegates/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /mvp/README.md: -------------------------------------------------------------------------------- 1 | # MVP 2 | This example uses standard iOS MVP pattern. 3 | 4 | 5 | ## Implementation 6 | The following image illustrates the bindings: 7 | 8 | 9 | ![scenario](example-scenario.png) 10 | 11 | - `ViewModel` **inputs** such as text field changes or `UITableView` row selection are defined as swift functions that are called in `ViewController` 12 | - `ViewModel` **outputs** are sending data to `ViewController` using delegation pattern 13 | 14 | ## Installation 15 | Clone the repository: 16 | 17 | `git clone git@github.com:tailec/ios-architecture.git` 18 | 19 | Navigate to `mvp` directory: 20 | 21 | `cd mvp` 22 | 23 | No `pod install` is required in this example. 24 | -------------------------------------------------------------------------------- /mvp/example-scenario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/mvp/example-scenario.png -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMClosures 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let viewModel = ReposViewModel(networkingService: NetworkingApi()) 21 | let viewController = ReposViewController(viewModel: viewModel) 22 | 23 | window.rootViewController = UINavigationController(rootViewController: viewController) 24 | window.makeKeyAndVisible() 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMClosures 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMClosures 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | protocol NetworkingService { 13 | @discardableResult func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask 14 | } 15 | 16 | final class NetworkingApi: NetworkingService { 17 | private let session = URLSession.shared 18 | 19 | @discardableResult 20 | func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask { 21 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 22 | let task = session.dataTask(with: request) { (data, _, _) in 23 | DispatchQueue.main.async { 24 | guard let data = data, 25 | let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { 26 | completion([]) 27 | return 28 | } 29 | completion(response.items) 30 | } 31 | } 32 | task.resume() 33 | return task 34 | } 35 | } 36 | 37 | fileprivate struct SearchResponse: Decodable { 38 | let items: [Repo] 39 | } 40 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/App/ReposScene/ReposViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposViewModel.swift 3 | // MVVMClosures 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class ReposViewModel { 12 | // Outputs 13 | var isRefreshing: ((Bool) -> Void)? 14 | var didUpdateRepos: (([RepoViewModel]) -> Void)? 15 | var didSelecteRepo: ((Int) -> Void)? 16 | 17 | private(set) var repos: [Repo] = [Repo]() { 18 | didSet { 19 | didUpdateRepos?(repos.map { RepoViewModel(repo: $0) }) 20 | } 21 | } 22 | 23 | private let throttle = Throttle(minimumDelay: 0.3) 24 | private var currentSearchNetworkTask: URLSessionDataTask? 25 | private var lastQuery: String? 26 | 27 | // Dependencies 28 | private let networkingService: NetworkingService 29 | 30 | init(networkingService: NetworkingService) { 31 | self.networkingService = networkingService 32 | } 33 | 34 | // Inputs 35 | func ready() { 36 | isRefreshing?(true) 37 | networkingService.searchRepos(withQuery: "swift") { [weak self] repos in 38 | guard let strongSelf = self else { return } 39 | strongSelf.finishSearching(with: repos) 40 | } 41 | } 42 | 43 | func didChangeQuery(_ query: String) { 44 | guard query.count > 2, 45 | query != lastQuery else { return } // distinct until changed 46 | lastQuery = query 47 | 48 | throttle.throttle { 49 | self.startSearchWithQuery(query) 50 | } 51 | } 52 | 53 | func didSelectRow(at indexPath: IndexPath) { 54 | if repos.isEmpty { return } 55 | didSelecteRepo?(repos[indexPath.item].id) 56 | } 57 | 58 | // Private 59 | private func startSearchWithQuery(_ query: String) { 60 | currentSearchNetworkTask?.cancel() // cancel previous pending request 61 | 62 | isRefreshing?(true) 63 | 64 | currentSearchNetworkTask = networkingService.searchRepos(withQuery: query) { [weak self] repos in 65 | guard let strongSelf = self else { return } 66 | strongSelf.finishSearching(with: repos) 67 | } 68 | } 69 | 70 | private func finishSearching(with repos: [Repo]) { 71 | isRefreshing?(false) 72 | self.repos = repos 73 | } 74 | } 75 | 76 | struct RepoViewModel { 77 | let name: String 78 | } 79 | 80 | extension RepoViewModel { 81 | init(repo: Repo) { 82 | self.name = repo.name 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/App/Utils/Throttle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Throttle.swift 3 | // MVVM Closures 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Throttle { 12 | private var workItem: DispatchWorkItem = DispatchWorkItem(block: {}) 13 | private var previousRun: Date = Date.distantPast 14 | private let queue: DispatchQueue 15 | private let delay: TimeInterval 16 | 17 | init(minimumDelay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) { 18 | self.delay = minimumDelay 19 | self.queue = queue 20 | } 21 | 22 | func throttle(_ block: @escaping () -> Void) { 23 | workItem.cancel() 24 | 25 | workItem = DispatchWorkItem() { 26 | [weak self] in 27 | self?.previousRun = Date() 28 | block() 29 | } 30 | 31 | let deltaDelay = previousRun.timeIntervalSinceNow > delay ? 0 : delay 32 | queue.asyncAfter(deadline: .now() + Double(deltaDelay), execute: workItem) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/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 | -------------------------------------------------------------------------------- /mvvm-closures/MVVMClosures/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | MVVMClosures 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 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /mvvm-closures/README.md: -------------------------------------------------------------------------------- 1 | # mvvm-closure 2 | This example uses standard swift closures. 3 | 4 | ## Implementation 5 | 6 | 7 | ## Installation 8 | Clone the repository: 9 | 10 | `git clone git@github.com:tailec/ios-architecture.git` 11 | 12 | Navigate to `mvvm-closures` directory: 13 | 14 | `cd mvvm-closures` 15 | 16 | No `pod install` is required in this example. 17 | -------------------------------------------------------------------------------- /mvvm-closures/example-scenario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/mvvm-closures/example-scenario.png -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 24/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let viewModel = ReposViewModel() 21 | let viewController = ReposViewController(viewModel: viewModel) 22 | 23 | window.rootViewController = UINavigationController(rootViewController: viewController) 24 | window.makeKeyAndVisible() 25 | } 26 | 27 | return true 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/App/Environments/AppEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppEnvironment.swift 3 | // MVVMFunctionsSubjectsObservables 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class AppEnvironment { 12 | static var current = Environment() 13 | } 14 | 15 | struct Environment { 16 | let networkingService: NetworkingService 17 | 18 | init(networkingService: NetworkingService = NetworkingApi()) { 19 | self.networkingService = networkingService 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol NetworkingService { 14 | func searchRepos(withQuery query: String) -> Observable<[Repo]> 15 | } 16 | 17 | final class NetworkingApi: NetworkingService { 18 | func searchRepos(withQuery query: String) -> Observable<[Repo]> { 19 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 20 | return URLSession.shared.rx.data(request: request) 21 | .map { data -> [Repo] in 22 | guard let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { return [] } 23 | return response.items 24 | } 25 | } 26 | } 27 | 28 | fileprivate struct SearchResponse: Decodable { 29 | let items: [Repo] 30 | } 31 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/App/Utils/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 10/18/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | import RxSwift 9 | import RxCocoa 10 | 11 | private struct ActivityToken : ObservableConvertibleType, Disposable { 12 | private let _source: Observable 13 | private let _dispose: Cancelable 14 | 15 | init(source: Observable, disposeAction: @escaping () -> ()) { 16 | _source = source 17 | _dispose = Disposables.create(with: disposeAction) 18 | } 19 | 20 | func dispose() { 21 | _dispose.dispose() 22 | } 23 | 24 | func asObservable() -> Observable { 25 | return _source 26 | } 27 | } 28 | 29 | /** 30 | Enables monitoring of sequence computation. 31 | If there is at least one sequence computation in progress, `true` will be sent. 32 | When all activities complete `false` will be sent. 33 | */ 34 | public class ActivityIndicator : SharedSequenceConvertibleType { 35 | public typealias E = Bool 36 | public typealias SharingStrategy = DriverSharingStrategy 37 | 38 | private let _lock = NSRecursiveLock() 39 | private let _relay = BehaviorRelay(value: 0) 40 | private let _loading: SharedSequence 41 | 42 | public init() { 43 | _loading = _relay.asDriver() 44 | .map { $0 > 0 } 45 | .distinctUntilChanged() 46 | } 47 | 48 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 49 | return Observable.using({ () -> ActivityToken in 50 | self.increment() 51 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) 52 | }) { t in 53 | return t.asObservable() 54 | } 55 | } 56 | 57 | private func increment() { 58 | _lock.lock() 59 | _relay.accept(_relay.value + 1) 60 | _lock.unlock() 61 | } 62 | 63 | private func decrement() { 64 | _lock.lock() 65 | _relay.accept(_relay.value - 1) 66 | _lock.unlock() 67 | } 68 | 69 | public func asSharedSequence() -> SharedSequence { 70 | return _loading 71 | } 72 | } 73 | 74 | extension ObservableConvertibleType { 75 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { 76 | return activityIndicator.trackActivityOfObservable(self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/App/Utils/UIViewController+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Rx.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | extension Reactive where Base: UIViewController { 13 | var viewWillAppear: ControlEvent { 14 | let source = self.methodInvoked(#selector(Base.viewWillAppear(_:))).map { _ in } 15 | return ControlEvent(events: source) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/App/Utils/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ViewModelType { 12 | associatedtype Input 13 | associatedtype Output 14 | 15 | func transform(input: Input) -> Output 16 | } 17 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/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 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/MVVMFunctionsSubjectsObservables/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'MVVMFunctionsSubjectsObservables' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | pod 'RxSwift', '4.4.0' 8 | pod 'RxCocoa', '4.4.0' 9 | end 10 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RxAtomic (4.4.0) 3 | - RxCocoa (4.4.0): 4 | - RxSwift (~> 4.0) 5 | - RxSwift (4.4.0): 6 | - RxAtomic (~> 4.4) 7 | 8 | DEPENDENCIES: 9 | - RxCocoa (= 4.4.0) 10 | - RxSwift (= 4.4.0) 11 | 12 | SPEC REPOS: 13 | https://github.com/cocoapods/specs.git: 14 | - RxAtomic 15 | - RxCocoa 16 | - RxSwift 17 | 18 | SPEC CHECKSUMS: 19 | RxAtomic: eacf60db868c96bfd63320e28619fe29c179656f 20 | RxCocoa: df63ebf7b9a70d6b4eeea407ed5dd4efc8979749 21 | RxSwift: 5976ecd04fc2fefd648827c23de5e11157faa973 22 | 23 | PODFILE CHECKSUM: 02ba7311780fd33b614317379d6b678f727a38aa 24 | 25 | COCOAPODS: 1.5.3 26 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/README.md: -------------------------------------------------------------------------------- 1 | # mvvm-rxswift-functions-subjects-observables 2 | This example uses RxSwift both observables and subjects as binding mechanism between `ViewModel` and `ViewController`. 3 | 4 | 5 | ## Implementation 6 | The following image illustrates the bindings: 7 | 8 | 9 | ![scenario](example-scenario.png) 10 | 11 | - `ViewModel` **inputs** such as text field changes or `UITableView` row selection are defined as swift functions which are simply wrappers for subjects in `ViewModel` 12 | - `ViewModel` **outputs** are defined as `Driver` traits 13 | 14 | 15 | ## Installation 16 | Clone the repository: 17 | 18 | `git clone git@github.com:tailec/ios-architecture.git` 19 | 20 | Navigate to `mvvm-rxswift-functions-subjects-observables` directory: 21 | 22 | `cd mvvm-rxswift-functions-subjects-observables` 23 | 24 | Install dependencies: 25 | 26 | `pod install` 27 | -------------------------------------------------------------------------------- /mvvm-functions-subjects-observables/example-scenario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/mvvm-functions-subjects-observables/example-scenario.png -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 24/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let viewModel = ReposViewModel(dependencies: ReposViewModel.Dependencies(networking: NetworkingApi())) 21 | let viewController = ReposViewController(viewModel: viewModel) 22 | 23 | window.rootViewController = UINavigationController(rootViewController: viewController) 24 | window.makeKeyAndVisible() 25 | } 26 | 27 | return true 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol NetworkingService { 14 | func searchRepos(withQuery query: String) -> Observable<[Repo]> 15 | } 16 | 17 | final class NetworkingApi: NetworkingService { 18 | func searchRepos(withQuery query: String) -> Observable<[Repo]> { 19 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 20 | return URLSession.shared.rx.data(request: request) 21 | .map { data -> [Repo] in 22 | guard let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { return [] } 23 | return response.items 24 | } 25 | } 26 | } 27 | 28 | fileprivate struct SearchResponse: Decodable { 29 | let items: [Repo] 30 | } 31 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/App/ReposScene/ReposViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoListViewModel.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | final class ReposViewModel: ViewModelType { 13 | struct Input { 14 | let ready: Driver 15 | let selectedIndex: Driver 16 | let searchText: Driver 17 | } 18 | 19 | struct Output { 20 | let loading: Driver 21 | let repos: Driver<[RepoViewModel]> 22 | let selectedRepoId: Driver 23 | } 24 | 25 | struct Dependencies { 26 | let networking: NetworkingService 27 | } 28 | 29 | private let dependencies: Dependencies 30 | 31 | init(dependencies: Dependencies) { 32 | self.dependencies = dependencies 33 | } 34 | 35 | func transform(input: Input) -> Output { 36 | let loading = ActivityIndicator() 37 | 38 | let initialRepos = input.ready 39 | .flatMap { _ in 40 | self.dependencies.networking 41 | .searchRepos(withQuery: "swift") 42 | .trackActivity(loading) 43 | .asDriver(onErrorJustReturn: []) 44 | } 45 | 46 | let searchRepos = input.searchText 47 | .filter { $0.count > 2} 48 | .throttle(0.3) 49 | .distinctUntilChanged() 50 | .flatMapLatest { query in 51 | self.dependencies.networking 52 | .searchRepos(withQuery: query) 53 | .trackActivity(loading) 54 | .asDriver(onErrorJustReturn: []) 55 | } 56 | 57 | let repos = Driver.merge(initialRepos, searchRepos) 58 | 59 | let repoViewModels = repos.map { $0.map { RepoViewModel(repo: $0)} } 60 | 61 | let selectedRepoId = input.selectedIndex 62 | .withLatestFrom(repos) { (indexPath, repos) in 63 | return repos[indexPath.item] 64 | } 65 | .map { $0.id } 66 | 67 | return Output(loading: loading.asDriver(), 68 | repos: repoViewModels, 69 | selectedRepoId: selectedRepoId) 70 | } 71 | } 72 | 73 | struct RepoViewModel { 74 | let name: String 75 | } 76 | 77 | extension RepoViewModel { 78 | init(repo: Repo) { 79 | self.name = repo.name 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/App/Utils/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 10/18/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | import RxSwift 9 | import RxCocoa 10 | 11 | private struct ActivityToken : ObservableConvertibleType, Disposable { 12 | private let _source: Observable 13 | private let _dispose: Cancelable 14 | 15 | init(source: Observable, disposeAction: @escaping () -> ()) { 16 | _source = source 17 | _dispose = Disposables.create(with: disposeAction) 18 | } 19 | 20 | func dispose() { 21 | _dispose.dispose() 22 | } 23 | 24 | func asObservable() -> Observable { 25 | return _source 26 | } 27 | } 28 | 29 | /** 30 | Enables monitoring of sequence computation. 31 | If there is at least one sequence computation in progress, `true` will be sent. 32 | When all activities complete `false` will be sent. 33 | */ 34 | public class ActivityIndicator : SharedSequenceConvertibleType { 35 | public typealias E = Bool 36 | public typealias SharingStrategy = DriverSharingStrategy 37 | 38 | private let _lock = NSRecursiveLock() 39 | private let _relay = BehaviorRelay(value: 0) 40 | private let _loading: SharedSequence 41 | 42 | public init() { 43 | _loading = _relay.asDriver() 44 | .map { $0 > 0 } 45 | .distinctUntilChanged() 46 | } 47 | 48 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 49 | return Observable.using({ () -> ActivityToken in 50 | self.increment() 51 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) 52 | }) { t in 53 | return t.asObservable() 54 | } 55 | } 56 | 57 | private func increment() { 58 | _lock.lock() 59 | _relay.accept(_relay.value + 1) 60 | _lock.unlock() 61 | } 62 | 63 | private func decrement() { 64 | _lock.lock() 65 | _relay.accept(_relay.value - 1) 66 | _lock.unlock() 67 | } 68 | 69 | public func asSharedSequence() -> SharedSequence { 70 | return _loading 71 | } 72 | } 73 | 74 | extension ObservableConvertibleType { 75 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { 76 | return activityIndicator.trackActivityOfObservable(self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/App/Utils/UIViewController+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Rx.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | extension Reactive where Base: UIViewController { 13 | var viewWillAppear: ControlEvent { 14 | let source = self.methodInvoked(#selector(Base.viewWillAppear(_:))).map { _ in } 15 | return ControlEvent(events: source) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/App/Utils/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ViewModelType { 12 | associatedtype Input 13 | associatedtype Output 14 | 15 | func transform(input: Input) -> Output 16 | } 17 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/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 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/MVVMPureObservables/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'MVVMPureObservables' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | pod 'RxSwift', '4.4.0' 8 | pod 'RxCocoa', '4.4.0' 9 | end 10 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RxAtomic (4.4.0) 3 | - RxCocoa (4.4.0): 4 | - RxSwift (~> 4.0) 5 | - RxSwift (4.4.0): 6 | - RxAtomic (~> 4.4) 7 | 8 | DEPENDENCIES: 9 | - RxCocoa (= 4.4.0) 10 | - RxSwift (= 4.4.0) 11 | 12 | SPEC REPOS: 13 | https://github.com/cocoapods/specs.git: 14 | - RxAtomic 15 | - RxCocoa 16 | - RxSwift 17 | 18 | SPEC CHECKSUMS: 19 | RxAtomic: eacf60db868c96bfd63320e28619fe29c179656f 20 | RxCocoa: df63ebf7b9a70d6b4eeea407ed5dd4efc8979749 21 | RxSwift: 5976ecd04fc2fefd648827c23de5e11157faa973 22 | 23 | PODFILE CHECKSUM: a45dab46ed6d137ee66a74f1aa51abb09f441bbb 24 | 25 | COCOAPODS: 1.5.3 26 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/README.md: -------------------------------------------------------------------------------- 1 | # mvvm-rxswift-pure-observables 2 | This example uses RxSwift observables as binding mechanism between `ViewModel` and `ViewController`. 3 | 4 | 5 | ## Implementation 6 | The following image illustrates the bindings: 7 | 8 | 9 | ![scenario](example-scenario.png) 10 | 11 | - `ViewModel` **inputs** such as text field changes or `UITableView` row selection are defined as `Driver` traits 12 | - `ViewModel` **outputs** are defined as `Driver` traits 13 | 14 | 15 | `ViewModelType` is a simple scaffolding for every `ViewModel` in this architecture and it clearly defines inputs and outputs as structs. 16 | 17 | ``` 18 | protocol ViewModelType { 19 | associatedtype Input 20 | associatedtype Output 21 | 22 | func transform(input: Input) -> Output 23 | } 24 | ``` 25 | 26 | Bindings are created when `ViewController` calls `func transform(input: Input) -> Output` function. 27 | 28 | 29 | ## Installation 30 | Clone the repository: 31 | 32 | `git clone git@github.com:tailec/ios-architecture.git` 33 | 34 | Navigate to `mvvm-rxswift-pure` directory: 35 | 36 | `cd mvvm-rxswift-pure` 37 | 38 | Install dependencies: 39 | 40 | `pod install` 41 | -------------------------------------------------------------------------------- /mvvm-rxswift-pure/example-scenario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/mvvm-rxswift-pure/example-scenario.png -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMFunctionsSubjectsObservables.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMFunctionsSubjectsObservables.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 24/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let viewModel = ReposViewModel(networkingService: NetworkingApi()) 21 | let viewController = ReposViewController(viewModel: viewModel) 22 | 23 | window.rootViewController = UINavigationController(rootViewController: viewController) 24 | window.makeKeyAndVisible() 25 | } 26 | 27 | return true 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol NetworkingService { 14 | func searchRepos(withQuery query: String) -> Observable<[Repo]> 15 | } 16 | 17 | final class NetworkingApi: NetworkingService { 18 | func searchRepos(withQuery query: String) -> Observable<[Repo]> { 19 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 20 | return URLSession.shared.rx.data(request: request) 21 | .map { data -> [Repo] in 22 | guard let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { return [] } 23 | return response.items 24 | } 25 | } 26 | } 27 | 28 | fileprivate struct SearchResponse: Decodable { 29 | let items: [Repo] 30 | } 31 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/App/ReposScene/ReposViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoListViewModel.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | final class ReposViewModel { 13 | // Inputs 14 | let viewWillAppearSubject = PublishSubject() 15 | let selectedIndexSubject = PublishSubject() 16 | let searchQuerySubject = BehaviorSubject(value: "") 17 | 18 | // Outputs 19 | var loading: Driver 20 | var repos: Driver<[RepoViewModel]> 21 | var selectedRepoId: Driver 22 | 23 | private let networkingService: NetworkingService 24 | 25 | init(networkingService: NetworkingService) { 26 | self.networkingService = networkingService 27 | 28 | let loading = ActivityIndicator() 29 | self.loading = loading.asDriver() 30 | 31 | let initialRepos = self.viewWillAppearSubject 32 | .asObservable() 33 | .flatMap { _ in 34 | networkingService 35 | .searchRepos(withQuery: "swift") 36 | .trackActivity(loading) 37 | } 38 | .asDriver(onErrorJustReturn: []) 39 | 40 | let searchRepos = self.searchQuerySubject 41 | .asObservable() 42 | .filter { $0.count > 2} 43 | .throttle(0.3, scheduler: MainScheduler.instance) 44 | .distinctUntilChanged() 45 | .flatMapLatest { query in 46 | networkingService 47 | .searchRepos(withQuery: query) 48 | .trackActivity(loading) 49 | } 50 | .asDriver(onErrorJustReturn: []) 51 | 52 | let repos = Driver.merge(initialRepos, searchRepos) 53 | 54 | self.repos = repos.map { $0.map { RepoViewModel(repo: $0)} } 55 | 56 | self.selectedRepoId = self.selectedIndexSubject 57 | .asObservable() 58 | .withLatestFrom(repos) { (indexPath, repos) in 59 | return repos[indexPath.item] 60 | } 61 | .map { $0.id } 62 | .asDriver(onErrorJustReturn: 0) 63 | } 64 | } 65 | 66 | struct RepoViewModel { 67 | let name: String 68 | } 69 | 70 | extension RepoViewModel { 71 | init(repo: Repo) { 72 | self.name = repo.name 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/App/Utils/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 10/18/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | import RxSwift 9 | import RxCocoa 10 | 11 | private struct ActivityToken : ObservableConvertibleType, Disposable { 12 | private let _source: Observable 13 | private let _dispose: Cancelable 14 | 15 | init(source: Observable, disposeAction: @escaping () -> ()) { 16 | _source = source 17 | _dispose = Disposables.create(with: disposeAction) 18 | } 19 | 20 | func dispose() { 21 | _dispose.dispose() 22 | } 23 | 24 | func asObservable() -> Observable { 25 | return _source 26 | } 27 | } 28 | 29 | /** 30 | Enables monitoring of sequence computation. 31 | If there is at least one sequence computation in progress, `true` will be sent. 32 | When all activities complete `false` will be sent. 33 | */ 34 | public class ActivityIndicator : SharedSequenceConvertibleType { 35 | public typealias E = Bool 36 | public typealias SharingStrategy = DriverSharingStrategy 37 | 38 | private let _lock = NSRecursiveLock() 39 | private let _relay = BehaviorRelay(value: 0) 40 | private let _loading: SharedSequence 41 | 42 | public init() { 43 | _loading = _relay.asDriver() 44 | .map { $0 > 0 } 45 | .distinctUntilChanged() 46 | } 47 | 48 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 49 | return Observable.using({ () -> ActivityToken in 50 | self.increment() 51 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) 52 | }) { t in 53 | return t.asObservable() 54 | } 55 | } 56 | 57 | private func increment() { 58 | _lock.lock() 59 | _relay.accept(_relay.value + 1) 60 | _lock.unlock() 61 | } 62 | 63 | private func decrement() { 64 | _lock.lock() 65 | _relay.accept(_relay.value - 1) 66 | _lock.unlock() 67 | } 68 | 69 | public func asSharedSequence() -> SharedSequence { 70 | return _loading 71 | } 72 | } 73 | 74 | extension ObservableConvertibleType { 75 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { 76 | return activityIndicator.trackActivityOfObservable(self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/App/Utils/UIViewController+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Rx.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | extension Reactive where Base: UIViewController { 13 | var viewWillAppear: ControlEvent { 14 | let source = self.methodInvoked(#selector(Base.viewWillAppear(_:))).map { _ in } 15 | return ControlEvent(events: source) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/App/Utils/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ViewModelType { 12 | associatedtype Input 13 | associatedtype Output 14 | 15 | func transform(input: Input) -> Output 16 | } 17 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/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 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/MVVMSubjectsObservables/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'MVVMSubjectsObservables' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | pod 'RxSwift', '4.4.0' 8 | pod 'RxCocoa', '4.4.0' 9 | end 10 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RxAtomic (4.4.0) 3 | - RxCocoa (4.4.0): 4 | - RxSwift (~> 4.0) 5 | - RxSwift (4.4.0): 6 | - RxAtomic (~> 4.4) 7 | 8 | DEPENDENCIES: 9 | - RxCocoa (= 4.4.0) 10 | - RxSwift (= 4.4.0) 11 | 12 | SPEC REPOS: 13 | https://github.com/cocoapods/specs.git: 14 | - RxAtomic 15 | - RxCocoa 16 | - RxSwift 17 | 18 | SPEC CHECKSUMS: 19 | RxAtomic: eacf60db868c96bfd63320e28619fe29c179656f 20 | RxCocoa: df63ebf7b9a70d6b4eeea407ed5dd4efc8979749 21 | RxSwift: 5976ecd04fc2fefd648827c23de5e11157faa973 22 | 23 | PODFILE CHECKSUM: a459f9267abc0be1ed401e2d0c3678a124b85e78 24 | 25 | COCOAPODS: 1.5.3 26 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/README.md: -------------------------------------------------------------------------------- 1 | # mvvm-rxswift-subjects-observables 2 | This example uses RxSwift observables and subjects as binding mechanism between `ViewModel` and `ViewController`. 3 | 4 | 5 | ## Implementation 6 | The following image illustrates the bindings: 7 | 8 | 9 | ![scenario](example-scenario.png) 10 | 11 | - `ViewModel` **inputs** such as text field changes or `UITableView` row selection are defined as subjects `ViewModel` 12 | - `ViewModel` **outputs** are defined as `Driver` traits 13 | 14 | 15 | ## Installation 16 | Clone the repository: 17 | 18 | `git clone git@github.com:tailec/ios-architecture.git` 19 | 20 | Navigate to `mvvm-rxswift-subjects-observables` repository: 21 | 22 | `cd mvvm-rxswift-subjects-observables` 23 | 24 | 25 | Install dependencies: 26 | 27 | `pod install` 28 | -------------------------------------------------------------------------------- /mvvm-rxswift-subjects-observables/example-scenario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/mvvm-rxswift-subjects-observables/example-scenario.png -------------------------------------------------------------------------------- /rxfeedback-mvc/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'rxfeedback-mvc' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | pod 'RxSwift', '4.4.0' 8 | pod 'RxCocoa', '4.4.0' 9 | pod 'RxFeedback', '2.0.0' 10 | 11 | 12 | end 13 | -------------------------------------------------------------------------------- /rxfeedback-mvc/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RxAtomic (4.4.0) 3 | - RxCocoa (4.4.0): 4 | - RxSwift (~> 4.0) 5 | - RxFeedback (2.0.0): 6 | - RxCocoa (~> 4.4) 7 | - RxSwift (~> 4.4) 8 | - RxSwift (4.4.0): 9 | - RxAtomic (~> 4.4) 10 | 11 | DEPENDENCIES: 12 | - RxCocoa (= 4.4.0) 13 | - RxFeedback (= 2.0.0) 14 | - RxSwift (= 4.4.0) 15 | 16 | SPEC REPOS: 17 | https://github.com/cocoapods/specs.git: 18 | - RxAtomic 19 | - RxCocoa 20 | - RxFeedback 21 | - RxSwift 22 | 23 | SPEC CHECKSUMS: 24 | RxAtomic: eacf60db868c96bfd63320e28619fe29c179656f 25 | RxCocoa: df63ebf7b9a70d6b4eeea407ed5dd4efc8979749 26 | RxFeedback: 49bb931caf6d05e315d5b5366c0991ad1474ea36 27 | RxSwift: 5976ecd04fc2fefd648827c23de5e11157faa973 28 | 29 | PODFILE CHECKSUM: e0c5ebdd5d2db9865faa5999eeb430eb6524b3b8 30 | 31 | COCOAPODS: 1.5.3 32 | -------------------------------------------------------------------------------- /rxfeedback-mvc/README.md: -------------------------------------------------------------------------------- 1 | # rxfeedback-mvc 2 | This example uses RxFeedback in MVC architecture. 3 | 4 | 5 | ## Implementation 6 | 7 | 8 | 9 | ## Installation 10 | Clone the repository: 11 | 12 | `git clone git@github.com:tailec/ios-architecture.git` 13 | 14 | Navigate to `rxfeedback-mvc` directory: 15 | 16 | `cd rxfeedback-mvc` 17 | 18 | Install dependencies: 19 | 20 | `pod install` 21 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 24/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let viewController = ReposViewController() 21 | 22 | window.rootViewController = UINavigationController(rootViewController: viewController) 23 | window.makeKeyAndVisible() 24 | } 25 | 26 | return true 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc/App/Models/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMPureObservables 4 | // 5 | // Created by krawiecp-home on 25/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol NetworkingService { 14 | func searchRepos(withQuery query: String) -> Observable<[Repo]> 15 | } 16 | 17 | final class NetworkingApi: NetworkingService { 18 | func searchRepos(withQuery query: String) -> Observable<[Repo]> { 19 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 20 | return URLSession.shared.rx.data(request: request) 21 | .map { data -> [Repo] in 22 | guard let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { return [] } 23 | return response.items 24 | } 25 | } 26 | } 27 | 28 | fileprivate struct SearchResponse: Decodable { 29 | let items: [Repo] 30 | } 31 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc/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 | -------------------------------------------------------------------------------- /rxfeedback-mvc/rxfeedback-mvc/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/1.png -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/2.png -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/3.png -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/4.png -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'tmdb-mvvm-pure' 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 | # Pods for tmdb-mvvm-pure 9 | pod 'RxSwift', '4.4.0' 10 | pod 'RxCocoa', '4.4.0' 11 | pod 'Nuke', '~> 7.0' 12 | 13 | target 'tmdb-mvvm-pureTests' do 14 | inherit! :search_paths 15 | # Pods for testing 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Nuke (7.5.2) 3 | - RxAtomic (4.4.0) 4 | - RxCocoa (4.4.0): 5 | - RxSwift (~> 4.0) 6 | - RxSwift (4.4.0): 7 | - RxAtomic (~> 4.4) 8 | 9 | DEPENDENCIES: 10 | - Nuke (~> 7.0) 11 | - RxCocoa (= 4.4.0) 12 | - RxSwift (= 4.4.0) 13 | 14 | SPEC REPOS: 15 | https://github.com/cocoapods/specs.git: 16 | - Nuke 17 | - RxAtomic 18 | - RxCocoa 19 | - RxSwift 20 | 21 | SPEC CHECKSUMS: 22 | Nuke: 0350d346a688426e8f2331253ef28dc2fc4f6178 23 | RxAtomic: eacf60db868c96bfd63320e28619fe29c179656f 24 | RxCocoa: df63ebf7b9a70d6b4eeea407ed5dd4efc8979749 25 | RxSwift: 5976ecd04fc2fefd648827c23de5e11157faa973 26 | 27 | PODFILE CHECKSUM: 54f1878677a8da01c22ba6c5ff5227317a18f8da 28 | 29 | COCOAPODS: 1.5.3 30 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/README.md: -------------------------------------------------------------------------------- 1 | # tmdb-mvvm-rxswift-pure 2 | 🔒 ** If you want to login, use username `iostest` and password `test`.** 3 | 4 | 5 | This example uses RxSwift observables as binding mechanism between `ViewModel` and `ViewController`. Also, it uses simple navigator for transitions between screns. 6 | 7 | | ![](1.png) | ![](2.png) | 8 | | --- | --- | 9 | | ![](3.png) | ![](4.png) | 10 | 11 | 12 | ## Implementation 13 | In progress. 14 | 15 | 16 | ## Installation 17 | Clone the repository: 18 | 19 | `git clone git@github.com:tailec/ios-architecture.git` 20 | 21 | Navigate to `tmdb-mvvm-pure` directory: 22 | 23 | `cd tmdb-mvvm-pure` 24 | 25 | Install dependencies: 26 | 27 | `pod install` 28 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | App.shared.startInterface(in: window) 21 | } 22 | 23 | return true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Models/Genre.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Genre.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Genre: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Models/Movie.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Movie.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Movie: Decodable { 12 | let id: Int 13 | let title: String 14 | let overview: String 15 | let genres: [Genre]? 16 | let posterUrl: String? 17 | let releaseDate: String 18 | let runtime: Int? 19 | let voteAverage: Double? 20 | let voteCount: Int? 21 | let status: String? 22 | 23 | enum CodingKeys: String, CodingKey { 24 | case id 25 | case title 26 | case overview 27 | case genres 28 | case posterUrl = "poster_path" 29 | case releaseDate = "release_date" 30 | case runtime 31 | case voteAverage = "vote_average" 32 | case voteCount = "vote_count" 33 | case status 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Models/Person.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Person.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct KnownFor: Decodable { 12 | let title: String? 13 | } 14 | 15 | struct Person: Decodable { 16 | let id: Int 17 | let name: String 18 | let profileUrl: String? 19 | let knownForTitles: [String]? 20 | 21 | enum CodingKeys: String, CodingKey { 22 | case id, name, profileUrl = "profile_path", knownFor = "known_for" 23 | } 24 | 25 | enum KnownForKeys: String, CodingKey { 26 | case knownForTitle = "title" 27 | } 28 | 29 | init(from decoder: Decoder) throws { 30 | let container = try decoder.container(keyedBy: CodingKeys.self) 31 | id = try container.decode(Int.self, forKey: .id) 32 | name = try container.decode(String.self, forKey: .name) 33 | profileUrl = try? container.decode(String.self, forKey: .profileUrl) 34 | var known = try container.nestedUnkeyedContainer(forKey: .knownFor) 35 | var titles: [String]? = [] 36 | while !known.isAtEnd { 37 | if let knownForDecodable = try? known.decode(KnownFor.self), 38 | let title = knownForDecodable.title { 39 | titles?.append(title) 40 | } else { 41 | titles = nil 42 | } 43 | } 44 | knownForTitles = titles 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Models/Show.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Show.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Show: Decodable { 12 | let id: Int 13 | let name: String 14 | let posterUrl: String 15 | let releaseDate: String 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case id, name, posterUrl = "poster_path", releaseDate = "first_air_date" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Networking/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClient.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | protocol HTTPClientProvider { 13 | func get(url: String) -> Observable 14 | func post(url: String, params: [String: Any]) -> Observable 15 | } 16 | 17 | final class HTTPClient: HTTPClientProvider { 18 | func get(url: String) -> Observable { 19 | guard let url = URL(string: url) else { return Observable.empty() } 20 | let request = URLRequest(url: url) 21 | return URLSession.shared.rx.data(request: request) 22 | .map { Optional.init($0) } 23 | .catchErrorJustReturn(nil) 24 | } 25 | 26 | func post(url: String, params: [String: Any]) -> Observable { 27 | guard let url = URL(string: url) else { return Observable.empty() } 28 | var request = URLRequest(url: url) 29 | request.httpMethod = "POST" 30 | request.setValue("application/json", forHTTPHeaderField: "Accept") 31 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 32 | let jsonData = try? JSONSerialization.data(withJSONObject: params, options: .prettyPrinted) 33 | request.httpBody = jsonData 34 | return URLSession.shared.rx.data(request: request) 35 | .map { Optional.init($0) } 36 | .catchErrorJustReturn(nil) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Networking/TMDBApiResponses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TMDBApiResponses.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct PeopleResponse: Decodable { 12 | let results: [Person] 13 | } 14 | 15 | struct ShowsResponse: Decodable { 16 | let results: [Show] 17 | } 18 | 19 | struct MoviesResponse: Decodable { 20 | let results: [Movie] 21 | } 22 | 23 | struct LoginResponse: Decodable { 24 | let success: Bool 25 | } 26 | 27 | struct AuthTokenResponse: Decodable { 28 | let requestToken: String 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case requestToken = "request_token" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Discover/DiscoverNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscoverNavigator.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol DiscoverNavigatable { 12 | func navigateToMovieDetailScreen(withMovieId id: Int, api: TMDBApiProvider) 13 | func navigateToPersonDetailScreen() 14 | func navigateToShowDetailScreen() 15 | } 16 | 17 | final class DiscoverNavigator: DiscoverNavigatable { 18 | private let navigationController: UINavigationController 19 | 20 | init(navigationController: UINavigationController) { 21 | self.navigationController = navigationController 22 | } 23 | 24 | func navigateToMovieDetailScreen(withMovieId id: Int, api: TMDBApiProvider) { 25 | let movieDetailNavigator = MovieDetailNavigator(navigationController: navigationController) 26 | let movieDetailViewModel = MovieDetailViewModel(dependencies: MovieDetailViewModel.Dependencies(id: id, 27 | api: api, 28 | navigator: movieDetailNavigator)) 29 | let movieDetailViewController = UIStoryboard.main.movieDetailViewController 30 | movieDetailViewController.viewModel = movieDetailViewModel 31 | 32 | navigationController.show(movieDetailViewController, sender: nil) 33 | } 34 | 35 | func navigateToPersonDetailScreen() { 36 | 37 | } 38 | 39 | func navigateToShowDetailScreen() { 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Discover/DiscoverViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscoverViewController.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class DiscoverViewController: UIViewController { 14 | var viewModel: DiscoverViewModel! 15 | 16 | @IBOutlet weak var carouselsView: DiscoverMainView! 17 | 18 | private let disposeBag = DisposeBag() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | bindViewModel() 23 | } 24 | 25 | override func viewWillAppear(_ animated: Bool) { 26 | super.viewWillAppear(animated) 27 | self.navigationController?.setNavigationBarHidden(true, animated: animated) 28 | } 29 | 30 | override var preferredStatusBarStyle: UIStatusBarStyle { 31 | return .lightContent 32 | } 33 | 34 | private func bindViewModel() { 35 | let input = DiscoverViewModel.Input(ready: rx.viewWillAppear.asDriver(), 36 | selected: carouselsView.selectedIndex.asDriver(onErrorJustReturn: (0, 0))) 37 | 38 | let output = viewModel.transform(input: input) 39 | 40 | output.loading 41 | .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible) 42 | .disposed(by: disposeBag) 43 | 44 | output.results 45 | .drive(onNext: { [weak self] caroselViewModel in 46 | guard let strongSelf = self else { return } 47 | strongSelf.carouselsView.setDataSource(caroselViewModel) 48 | strongSelf.carouselsView.reloadData() 49 | }) 50 | .disposed(by: disposeBag) 51 | 52 | output.selected 53 | .drive() 54 | .disposed(by: disposeBag) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Discover/Views/CarouselSectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselSection.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 28/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CarouselSectionCell: UITableViewCell { 12 | @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint! 13 | @IBOutlet weak var titleLabel: UILabel! 14 | @IBOutlet weak var subtitleLabel: UILabel! 15 | @IBOutlet weak var collectionView: UICollectionView! 16 | 17 | override func awakeFromNib() { 18 | let layout: UICollectionViewFlowLayout = { 19 | let layout = UICollectionViewFlowLayout() 20 | layout.estimatedItemSize = CGSize(width: 140, height: 235) 21 | layout.scrollDirection = .horizontal 22 | layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize 23 | return layout 24 | }() 25 | 26 | collectionView.register(UINib(nibName: String(describing: MovieCell.self), bundle: nil), 27 | forCellWithReuseIdentifier: String(describing: MovieCell.self)) 28 | collectionView.collectionViewLayout = layout 29 | collectionViewHeightConstraint.constant = MovieCell.height(forWidth: 140) 30 | collectionViewHeightConstraint.isActive = true 31 | collectionView.contentInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) 32 | } 33 | } 34 | 35 | extension CarouselSectionCell { 36 | var collectionViewOffset: CGFloat { 37 | get { return collectionView.contentOffset.x } 38 | set { collectionView.contentOffset.x = newValue } 39 | } 40 | 41 | func setCollectionViewDataSourceDelegate(_ dataSourceDelegate: D, forRow row: Int) { 42 | collectionView.delegate = dataSourceDelegate 43 | collectionView.dataSource = dataSourceDelegate 44 | collectionView.tag = row 45 | collectionView.setContentOffset(collectionView.contentOffset, animated:false) // Stops collection view if it was scrolling. 46 | collectionView.reloadData() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Discover/Views/MovieCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieCell.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 28/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MovieCell: UICollectionViewCell { 12 | @IBOutlet weak var imageView: UIImageView! 13 | @IBOutlet weak var titleLabel: UILabel! 14 | @IBOutlet weak var subtitleLabel: UILabel! 15 | 16 | private struct Constants { 17 | static let maxHeight: CGFloat = 400 18 | } 19 | 20 | private static let sizingCell = UINib(nibName: String(describing: MovieCell.self), bundle: nil) 21 | .instantiate(withOwner: nil, options: nil).first! as! MovieCell 22 | 23 | static func height(forWidth width: CGFloat) -> CGFloat { 24 | sizingCell.prepareForReuse() 25 | sizingCell.layoutIfNeeded() 26 | 27 | var fittingSize = UIView.layoutFittingCompressedSize 28 | fittingSize.width = width 29 | let size = sizingCell.contentView.systemLayoutSizeFitting(fittingSize, 30 | withHorizontalFittingPriority: .required, 31 | verticalFittingPriority: .defaultLow) 32 | 33 | guard size.height < Constants.maxHeight else { 34 | return Constants.maxHeight 35 | } 36 | 37 | return size.height 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Login/LoginNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginNavigator.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol LoginNavigatable { 12 | func toMain() 13 | } 14 | 15 | final class LoginNavigator { 16 | private let navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func toMain() { 23 | navigationController.dismiss(animated: true, completion: nil) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Login/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModel.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | enum LoginResult { 13 | case success 14 | case failure 15 | } 16 | 17 | final class LoginViewModel: ViewModelType { 18 | struct Input { 19 | let username: Driver 20 | let password: Driver 21 | let loginTaps: Signal 22 | } 23 | 24 | struct Output { 25 | let enabled: Driver 26 | let loading: Driver 27 | let result: Driver 28 | } 29 | 30 | struct Dependencies { 31 | let api: TMDBApiProvider 32 | let navigator: LoginNavigator 33 | } 34 | 35 | private let dependencies: Dependencies 36 | 37 | init(dependencies: Dependencies) { 38 | self.dependencies = dependencies 39 | } 40 | 41 | func transform(input: LoginViewModel.Input) -> LoginViewModel.Output { 42 | let isUsernameValid = input.username 43 | .map { $0.count > 0 } 44 | let isPasswordValid = input.password 45 | .map { $0.count >= 4 } 46 | let enabled = Driver.combineLatest(isUsernameValid, isPasswordValid) { $0 && $1 } 47 | 48 | let loadingIndicator = ActivityIndicator() 49 | let loading = loadingIndicator.asDriver() 50 | 51 | let usernameAndPassword = Driver.combineLatest(input.username, input.password) { ($0, $1 )} 52 | .asObservable() 53 | let result = input.loginTaps 54 | .asObservable() 55 | .withLatestFrom(usernameAndPassword) 56 | .flatMapLatest { pair -> Observable in 57 | let (username, password) = pair 58 | return self.dependencies.api.login(withUsername: username, password: password) 59 | .trackActivity(loadingIndicator) 60 | } 61 | .map { $0 ? LoginResult.success : LoginResult.failure } 62 | .asDriver(onErrorJustReturn: .failure) 63 | .do(onNext: { [weak self] loginResult in 64 | guard loginResult == LoginResult.success, 65 | let strongSelf = self else { return } 66 | strongSelf.dependencies.navigator.toMain() 67 | }) 68 | 69 | return Output(enabled: enabled, 70 | loading: loading, 71 | result: result) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/MovieDetail/MovieDetailNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieDetailNavigator.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 28/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol MovieDetailNavigatable { 12 | func goBack() 13 | } 14 | 15 | final class MovieDetailNavigator: MovieDetailNavigatable { 16 | private let navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | func goBack() { 22 | navigationController.popViewController(animated: true) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/MovieDetail/MovieDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieDetailViewController.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 28/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Nuke 13 | 14 | final class MovieDetailViewController: UIViewController { 15 | var viewModel: MovieDetailViewModel! 16 | 17 | @IBOutlet weak var headerView: MovieDetailHeaderView! 18 | @IBOutlet weak var tipsView: MovieDetailTipsView! 19 | @IBOutlet weak var posterImageView: GradientImageView! 20 | @IBOutlet weak var backButton: UIButton! 21 | 22 | private let disposeBag = DisposeBag() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | bindViewModel() 27 | } 28 | 29 | override func viewWillAppear(_ animated: Bool) { 30 | super.viewWillAppear(animated) 31 | self.navigationController?.setNavigationBarHidden(true, animated: animated) 32 | } 33 | 34 | override var preferredStatusBarStyle: UIStatusBarStyle { 35 | return .lightContent 36 | } 37 | 38 | private func bindViewModel() { 39 | let input = MovieDetailViewModel.Input(ready: rx.viewWillAppear.asDriver(), 40 | backTrigger: backButton.rx.tap.asDriver()) 41 | 42 | let output = viewModel.transform(input: input) 43 | 44 | output.data 45 | .drive(onNext: { [weak self] data in 46 | guard let data = data, 47 | let strongSelf = self else { return } 48 | strongSelf.headerView.configure(with: data) 49 | strongSelf.tipsView.configure(with: data) 50 | if let url = data.posterUrl { 51 | Nuke.loadImage(with: URL(string: url)!, into: strongSelf.posterImageView) 52 | } 53 | }) 54 | .disposed(by: disposeBag) 55 | 56 | output.back 57 | .drive() 58 | .disposed(by: disposeBag) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/MovieDetail/Views/GradientImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientImageView.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class GradientImageView: UIImageView { 12 | override init(image: UIImage?) { 13 | super.init(image: image) 14 | setup() 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | super.init(coder: aDecoder) 19 | setup() 20 | } 21 | 22 | private func setup() { 23 | layer.backgroundColor = UIColor.black.cgColor 24 | layer.opacity = 0.1 25 | 26 | let gradient = CAGradientLayer() 27 | gradient.frame = bounds 28 | gradient.colors = [UIColor.clear.cgColor, UIColor(red: 18/255, green: 18/255, blue: 18/255, alpha: 1.0).cgColor, UIColor(red: 18/255, green: 18/255, blue: 18/255, alpha: 1.0).cgColor, UIColor.clear.cgColor] 29 | gradient.locations = [0, 0.1, 0.9, 1] 30 | layer.mask = gradient 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/MovieDetail/Views/MovieDetailHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieDetailHeaderView.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 28/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class MovieDetailHeaderView: UIView { 12 | @IBOutlet weak var titleLabel: UILabel! 13 | @IBOutlet weak var releaseDateLabel: UILabel! 14 | @IBOutlet weak var genresLabel: UILabel! 15 | @IBOutlet weak var runtimeLabel: UILabel! 16 | @IBOutlet weak var voteAverageLabel: UILabel! 17 | @IBOutlet weak var overviewLabel: UILabel! 18 | @IBOutlet var contentView: UIView! 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | setup() 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | super.init(coder: aDecoder) 27 | setup() 28 | } 29 | 30 | func configure(withTitle title: String, 31 | releaseDate: String, 32 | genres: String, 33 | runtime: String, 34 | voteAverage: String, 35 | overview: String) { 36 | titleLabel.text = title 37 | releaseDateLabel.text = releaseDate 38 | genresLabel.text = genres 39 | runtimeLabel.text = runtime 40 | voteAverageLabel.text = voteAverage 41 | overviewLabel.text = overview 42 | } 43 | 44 | private func setup() { 45 | Bundle.main.loadNibNamed("MovieDetailHeaderView", owner: self, options: nil) 46 | addSubview(contentView) 47 | 48 | contentView.frame = self.bounds 49 | contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth] 50 | 51 | } 52 | } 53 | 54 | extension MovieDetailHeaderView { 55 | func configure(with data: MovieDetailData) { 56 | configure(withTitle: data.title, 57 | releaseDate: data.releaseDate, 58 | genres: data.genres, 59 | runtime: data.runtime, 60 | voteAverage: data.voteAverage, 61 | overview: data.overview) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/MovieDetail/Views/MovieDetailTipsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieDetailTipsView.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 29/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class MovieDetailTipsView: UIView { 12 | @IBOutlet weak var voteCountLabel: UILabel! 13 | @IBOutlet weak var statusLabel: UILabel! 14 | @IBOutlet var contentView: UIView! 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | setup() 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | super.init(coder: aDecoder) 22 | setup() 23 | } 24 | 25 | func configure(withVoteCount voteCount: String, 26 | status: String) { 27 | self.voteCountLabel.text = voteCount 28 | self.statusLabel.text = status 29 | } 30 | 31 | private func setup() { 32 | Bundle.main.loadNibNamed("MovieDetailTipsView", owner: self, options: nil) 33 | addSubview(contentView) 34 | 35 | contentView.frame = self.bounds 36 | contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth] 37 | 38 | } 39 | } 40 | 41 | extension MovieDetailTipsView { 42 | func configure(with data: MovieDetailData) { 43 | configure(withVoteCount: data.voteCount, 44 | status: data.status) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Search/SearchNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchNavigator.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 29/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol SearchNavigatable { 12 | func navigateToMovieDetailScreen(withMovieId id: Int, api: TMDBApiProvider) 13 | } 14 | 15 | final class SearchNavigator: SearchNavigatable { 16 | private let navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func navigateToMovieDetailScreen(withMovieId id: Int, api: TMDBApiProvider) { 23 | let movieDetailNavigator = MovieDetailNavigator(navigationController: navigationController) 24 | let movieDetailViewModel = MovieDetailViewModel(dependencies: MovieDetailViewModel.Dependencies(id: id, 25 | api: api, 26 | navigator: movieDetailNavigator)) 27 | let movieDetailViewController = UIStoryboard.main.movieDetailViewController 28 | movieDetailViewController.viewModel = movieDetailViewModel 29 | 30 | navigationController.show(movieDetailViewController, sender: nil) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Search/SearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 29/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class SearchViewController: UIViewController { 14 | var viewModel: SearchViewModel! 15 | 16 | @IBOutlet weak var searchTextField: UITextField! 17 | @IBOutlet weak var segmentedControl: UISegmentedControl! 18 | @IBOutlet weak var tableView: UITableView! { 19 | didSet { 20 | tableView.register(UINib(nibName: String(describing: SearchCell.self), bundle: nil), 21 | forCellReuseIdentifier: String(describing: SearchCell.self)) 22 | } 23 | } 24 | 25 | private let disposeBag = DisposeBag() 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | bindViewModel() 30 | } 31 | 32 | override func viewWillAppear(_ animated: Bool) { 33 | super.viewWillAppear(animated) 34 | self.navigationController?.setNavigationBarHidden(true, animated: animated) 35 | } 36 | 37 | override var preferredStatusBarStyle: UIStatusBarStyle { 38 | return .lightContent 39 | } 40 | 41 | private func bindViewModel() { 42 | let input = SearchViewModel.Input(searchText: searchTextField.rx.text.orEmpty.asDriver(), 43 | selectedCategoryIndex: segmentedControl.rx.value.asDriver(), 44 | selected: tableView.rx.itemSelected.asDriver()) 45 | 46 | let output = viewModel.transform(input: input) 47 | 48 | output.switchHidden 49 | .drive(segmentedControl.rx.isHidden) 50 | .disposed(by: disposeBag) 51 | 52 | output.loading 53 | .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible) 54 | .disposed(by: disposeBag) 55 | 56 | 57 | output.results 58 | .drive(tableView.rx.items(cellIdentifier: String(describing: SearchCell.self), cellType: SearchCell.self)) { (row, element, cell) in 59 | cell.configure(withSearchResultItemViewModel: element) 60 | } 61 | .disposed(by: disposeBag) 62 | 63 | output.selectedDone 64 | .drive() 65 | .disposed(by: disposeBag) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Scenes/Search/Views/SearchCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchCell.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 30/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Nuke 11 | 12 | class SearchCell: UITableViewCell { 13 | @IBOutlet weak var titleImageView: UIImageView! 14 | @IBOutlet weak var titleLabel: UILabel! 15 | @IBOutlet weak var subtitleLabel: UILabel! 16 | 17 | override func awakeFromNib() { 18 | selectionStyle = .none 19 | } 20 | 21 | func configure(withImageUrl url: String?, 22 | title: String, 23 | subtitle: String) { 24 | if let url = url { 25 | Nuke.loadImage(with: URL(string: url)!, into: titleImageView) 26 | } 27 | titleLabel.text = title 28 | subtitleLabel.text = subtitle 29 | } 30 | } 31 | 32 | extension SearchCell { 33 | func configure(withSearchResultItemViewModel item: SearchResultItemViewModel) { 34 | configure(withImageUrl: item.imageUrl, 35 | title: item.title, 36 | subtitle: item.subtitle) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Utils/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 10/18/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | import RxSwift 9 | import RxCocoa 10 | 11 | private struct ActivityToken : ObservableConvertibleType, Disposable { 12 | private let _source: Observable 13 | private let _dispose: Cancelable 14 | 15 | init(source: Observable, disposeAction: @escaping () -> ()) { 16 | _source = source 17 | _dispose = Disposables.create(with: disposeAction) 18 | } 19 | 20 | func dispose() { 21 | _dispose.dispose() 22 | } 23 | 24 | func asObservable() -> Observable { 25 | return _source 26 | } 27 | } 28 | 29 | /** 30 | Enables monitoring of sequence computation. 31 | If there is at least one sequence computation in progress, `true` will be sent. 32 | When all activities complete `false` will be sent. 33 | */ 34 | public class ActivityIndicator : SharedSequenceConvertibleType { 35 | public typealias E = Bool 36 | public typealias SharingStrategy = DriverSharingStrategy 37 | 38 | private let _lock = NSRecursiveLock() 39 | private let _relay = BehaviorRelay(value: 0) 40 | private let _loading: SharedSequence 41 | 42 | public init() { 43 | _loading = _relay.asDriver() 44 | .map { $0 > 0 } 45 | .distinctUntilChanged() 46 | } 47 | 48 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 49 | return Observable.using({ () -> ActivityToken in 50 | self.increment() 51 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) 52 | }) { t in 53 | return t.asObservable() 54 | } 55 | } 56 | 57 | private func increment() { 58 | _lock.lock() 59 | _relay.accept(_relay.value + 1) 60 | _lock.unlock() 61 | } 62 | 63 | private func decrement() { 64 | _lock.lock() 65 | _relay.accept(_relay.value - 1) 66 | _lock.unlock() 67 | } 68 | 69 | public func asSharedSequence() -> SharedSequence { 70 | return _loading 71 | } 72 | } 73 | 74 | extension ObservableConvertibleType { 75 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { 76 | return activityIndicator.trackActivityOfObservable(self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Utils/UIStoryboard+ViewControllers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStoryboard+ViewControllers.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIStoryboard { 12 | static var main: UIStoryboard { 13 | return UIStoryboard(name: "Main", bundle: nil) 14 | } 15 | } 16 | 17 | extension UIStoryboard { 18 | var loginViewController: LoginViewController { 19 | guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "LoginViewController") as? LoginViewController else { 20 | fatalError("LoginViewController couldn't be found in Storyboard file") 21 | } 22 | return vc 23 | } 24 | 25 | var discoverViewController: DiscoverViewController { 26 | guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "DiscoverViewController") as? DiscoverViewController else { 27 | fatalError("DiscoverViewController couldn't be found in Storyboard file") 28 | } 29 | return vc 30 | } 31 | 32 | var movieDetailViewController: MovieDetailViewController { 33 | guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "MovieDetailViewController") as? MovieDetailViewController else { 34 | fatalError("MovieDetailViewController couldn't be found in Storyboard file") 35 | } 36 | return vc 37 | } 38 | 39 | var searchViewController: SearchViewController { 40 | guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "SearchViewController") as? SearchViewController else { 41 | fatalError("SearchViewController couldn't be found in Storyboard file") 42 | } 43 | return vc 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Utils/UIViewController+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Rx.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 29/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | extension Reactive where Base: UIViewController { 13 | var viewWillAppear: ControlEvent { 14 | let source = self.methodInvoked(#selector(Base.viewWillAppear(_:))).map { _ in } 15 | return ControlEvent(events: source) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/App/Utils/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // tmdb-mvvm-pure 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ViewModelType { 12 | associatedtype Input 13 | associatedtype Output 14 | 15 | func transform(input: Input) -> Output 16 | } 17 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/aquaman.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "aquaman.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/aquaman.imageset/aquaman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/aquaman.imageset/aquaman.jpg -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/back_arrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icons8-left-100 (1).png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/back_arrow.imageset/icons8-left-100 (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/back_arrow.imageset/icons8-left-100 (1).png -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/sample.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sample.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/sample.imageset/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/sample.imageset/sample.jpg -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/search_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icons8-search-100.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/search_icon.imageset/icons8-search-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/search_icon.imageset/icons8-search-100.png -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/star_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icons8-star-filled-96.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/star_icon.imageset/icons8-star-filled-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailec/ios-architecture/2ca8b983175f6fc57d4ed468075ce44b6cf80434/tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Assets.xcassets/star_icon.imageset/icons8-star-filled-96.png -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/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 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pure/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | $(PRODUCT_NAME) 20 | CFBundlePackageType 21 | APPL 22 | CFBundleShortVersionString 23 | 1.0 24 | CFBundleVersion 25 | 1 26 | LSRequiresIPhoneOS 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pureTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /tmdb-mvvm-rxswift-pure/tmdb-mvvm-pureTests/tmdb_mvvm_pureTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // tmdb_mvvm_pureTests.swift 3 | // tmdb-mvvm-pureTests 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import tmdb_mvvm_pure 11 | 12 | class tmdb_mvvm_pureTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /viper/README.md: -------------------------------------------------------------------------------- 1 | # viper 2 | This example uses VIPER architecture. 3 | 4 | ## Implementation 5 | 6 | 7 | ## Installation 8 | Clone the repository: 9 | 10 | `git clone git@github.com:tailec/ios-architecture.git` 11 | 12 | Navigate to `viper` directory: 13 | 14 | `cd viper` 15 | 16 | No `pod install` is required in this example. 17 | -------------------------------------------------------------------------------- /viper/VIPER.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /viper/VIPER.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /viper/VIPER/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVMClosures 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | 19 | if let window = window { 20 | let view = ReposWireframe.createReposModule() 21 | window.rootViewController = view 22 | window.makeKeyAndVisible() 23 | } 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /viper/VIPER/App/Entities/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // MVVMClosures 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Decodable { 12 | let id: Int 13 | let name: String 14 | } 15 | -------------------------------------------------------------------------------- /viper/VIPER/App/Modules/Repos/CancellableReposFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CancellableReposFetcher.swift 3 | // VIPER 4 | // 5 | // Created by krawiecp-home on 20/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CancellableReposFetchable { 12 | func fetchRepos(withQuery query: String, completion: @escaping (([Repo]) -> ())) 13 | } 14 | 15 | final class CancellableReposFetcher: CancellableReposFetchable { 16 | private var currentSearchNetworkTask: URLSessionDataTask? 17 | private let networkingService: NetworkingService 18 | 19 | init(networkingService: NetworkingService = NetworkingApi()) { 20 | self.networkingService = networkingService 21 | } 22 | 23 | func fetchRepos(withQuery query: String, completion: @escaping (([Repo]) -> ())) { 24 | currentSearchNetworkTask?.cancel() // cancel previous pending request 25 | 26 | _ = currentSearchNetworkTask = networkingService.searchRepos(withQuery: query) { repos in 27 | completion(repos) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /viper/VIPER/App/Modules/Repos/ReposInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposInteractor.swift 3 | // VIPER 4 | // 5 | // Created by krawiecp-home on 19/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class ReposInteractor: ReposInteractorInputsType { 12 | weak var presenter: ReposInteractorOutputsType? 13 | 14 | // Dependencies 15 | private let dataManager: ReposRemoteDataManager 16 | private let validator = ThrottledTextFieldValidator() 17 | 18 | init(dataManager: ReposRemoteDataManager = ReposRemoteDataManager()) { 19 | self.dataManager = dataManager 20 | } 21 | 22 | // ReposInteractorInputsProtocol 23 | func fetchRepos(for query: String) { 24 | if (query.isEmpty) { 25 | startFetching(for: "rxswift") 26 | } else { 27 | validator.validate(query: query) { [weak self] query in 28 | guard let strongSelf = self, 29 | let query = query else { return } 30 | strongSelf.startFetching(for: query) 31 | } 32 | } 33 | } 34 | 35 | func fetchInitialRepos() { 36 | startFetching(for: "rxswift") 37 | } 38 | 39 | private func startFetching(for query: String) { 40 | dataManager.fetchRepos(for: query, completion: { [weak self] repos in 41 | guard let strongSelf = self else { return } 42 | strongSelf.presenter?.didRetrieveRepos(repos) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /viper/VIPER/App/Modules/Repos/ReposPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposPresenter.swift 3 | // VIPER 4 | // 5 | // Created by krawiecp-home on 19/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class ReposPresenter: ReposPresenterType, ReposInteractorOutputsType { 12 | weak var view: ReposViewType? 13 | var interactor: ReposInteractorInputsType? 14 | var wireframe: ReposWireframeType? 15 | 16 | private var repos = [Repo]() 17 | 18 | func onViewDidLoad() { 19 | view?.showLoading() 20 | interactor?.fetchInitialRepos() 21 | } 22 | 23 | func didRetrieveRepos(_ repos: [Repo]) { 24 | self.repos = repos 25 | view?.hideLoading() 26 | view?.didReceiveRepos() 27 | } 28 | 29 | func didChangeQuery(_ query: String?) { 30 | guard let query = query else { return } 31 | view?.showLoading() 32 | interactor?.fetchRepos(for: query) 33 | } 34 | 35 | func didSelectRow(_ indexPath: IndexPath) { 36 | let id = repos[indexPath.row].id 37 | view?.displayAlert(for: id) 38 | } 39 | 40 | func numberOfListItems() -> Int { 41 | return repos.count 42 | } 43 | 44 | func listItem(at index: Int) -> RepoViewModel { 45 | let item = repos.map { RepoViewModel(repo: $0) } 46 | return item[index] 47 | } 48 | } 49 | 50 | struct RepoViewModel { 51 | let name: String 52 | } 53 | 54 | extension RepoViewModel { 55 | init(repo: Repo) { 56 | self.name = repo.name 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /viper/VIPER/App/Modules/Repos/ReposProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposProtocols.swift 3 | // VIPER 4 | // 5 | // Created by krawiecp-home on 19/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ReposViewType: class { 12 | var presenter: ReposPresenterType? { get set } 13 | func didReceiveRepos() 14 | func showLoading() 15 | func hideLoading() 16 | func displayAlert(for id: Int) 17 | } 18 | 19 | protocol ReposWireframeType: class { 20 | static func createReposModule() -> UIViewController 21 | } 22 | 23 | protocol ReposPresenterType: class { 24 | var view: ReposViewType? { get set } 25 | var interactor: ReposInteractorInputsType? { get set } 26 | var wireframe: ReposWireframeType? { get set } 27 | 28 | func onViewDidLoad() 29 | func didChangeQuery(_ query: String?) 30 | func didSelectRow(_ indexPath: IndexPath) 31 | 32 | func numberOfListItems() -> Int 33 | func listItem(at index: Int) -> RepoViewModel 34 | } 35 | 36 | protocol ReposInteractorInputsType: class { 37 | var presenter: ReposInteractorOutputsType? { get set } 38 | func fetchRepos(for query: String) 39 | func fetchInitialRepos() 40 | } 41 | 42 | protocol ReposInteractorOutputsType: class { 43 | func didRetrieveRepos(_ repos: [Repo]) 44 | } 45 | 46 | protocol ReposRemoteDataManagerType: class { 47 | func fetchRepos(for query: String, completion: @escaping ([Repo]) -> ()) 48 | } 49 | 50 | -------------------------------------------------------------------------------- /viper/VIPER/App/Modules/Repos/ReposRemoteDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposRemoteDataManager.swift 3 | // VIPER 4 | // 5 | // Created by krawiecp-home on 20/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class ReposRemoteDataManager: ReposRemoteDataManagerType { 12 | private let networkingService: CancellableReposFetchable 13 | 14 | init(networkingService: CancellableReposFetchable = CancellableReposFetcher()) { 15 | self.networkingService = networkingService 16 | } 17 | 18 | func fetchRepos(for query: String, completion: @escaping ([Repo]) -> ()) { 19 | networkingService.fetchRepos(withQuery: query, completion: completion) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /viper/VIPER/App/Modules/Repos/ReposWireframe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReposWireframe.swift 3 | // VIPER 4 | // 5 | // Created by krawiecp-home on 19/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class ReposWireframe: ReposWireframeType { 12 | static func createReposModule() -> UIViewController { 13 | let view: ReposViewType = ReposViewController() 14 | let presenter: ReposPresenterType & ReposInteractorOutputsType = ReposPresenter() 15 | let interactor: ReposInteractorInputsType = ReposInteractor() 16 | let wireframe: ReposWireframeType = ReposWireframe() 17 | 18 | view.presenter = presenter 19 | presenter.view = view 20 | presenter.wireframe = wireframe 21 | presenter.interactor = interactor 22 | interactor.presenter = presenter 23 | 24 | if let view = view as? ReposViewController { 25 | return UINavigationController(rootViewController: view) 26 | } else { 27 | return UIViewController() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /viper/VIPER/App/Modules/Repos/ThrottledTextFieldValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThrottledTextFieldValidator.swift 3 | // VIPER 4 | // 5 | // Created by krawiecp-home on 20/02/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class ThrottledTextFieldValidator { 12 | private var lastQuery: String? 13 | private let throttle: Throttle 14 | private let validationRule: ((String) -> Bool) 15 | 16 | init(throttle: Throttle = Throttle(minimumDelay: 0.3), 17 | validationRule: @escaping ((String) -> Bool) = { query in return query.count > 2 }) { 18 | self.throttle = throttle 19 | self.validationRule = validationRule 20 | } 21 | 22 | func validate(query: String, 23 | completion: @escaping ((String?) -> ())) { 24 | guard validationRule(query), 25 | distinctUntilChanged(query) else { 26 | completion(nil) 27 | return 28 | } 29 | throttle.throttle { 30 | completion(query) 31 | } 32 | } 33 | 34 | private func distinctUntilChanged(_ query: String) -> Bool { 35 | let valid = lastQuery != query 36 | lastQuery = query 37 | return valid 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /viper/VIPER/App/Networking/NetworkingApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingApi.swift 3 | // MVVMClosures 4 | // 5 | // Created by krawiecp-home on 26/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | protocol NetworkingService { 13 | @discardableResult func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask 14 | } 15 | 16 | final class NetworkingApi: NetworkingService { 17 | private let session = URLSession.shared 18 | 19 | @discardableResult 20 | func searchRepos(withQuery query: String, completion: @escaping ([Repo]) -> ()) -> URLSessionDataTask { 21 | let request = URLRequest(url: URL(string: "https://api.github.com/search/repositories?q=\(query)")!) 22 | let task = session.dataTask(with: request) { (data, _, _) in 23 | DispatchQueue.main.async { 24 | guard let data = data, 25 | let response = try? JSONDecoder().decode(SearchResponse.self, from: data) else { 26 | completion([]) 27 | return 28 | } 29 | completion(response.items) 30 | } 31 | } 32 | task.resume() 33 | return task 34 | } 35 | } 36 | 37 | fileprivate struct SearchResponse: Decodable { 38 | let items: [Repo] 39 | } 40 | -------------------------------------------------------------------------------- /viper/VIPER/App/Utils/Throttle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Throttle.swift 3 | // MVVM Closures 4 | // 5 | // Created by krawiecp-home on 27/01/2019. 6 | // Copyright © 2019 tailec. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Throttle { 12 | private var workItem: DispatchWorkItem = DispatchWorkItem(block: {}) 13 | private var previousRun: Date = Date.distantPast 14 | private let queue: DispatchQueue 15 | private let delay: TimeInterval 16 | 17 | init(minimumDelay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) { 18 | self.delay = minimumDelay 19 | self.queue = queue 20 | } 21 | 22 | func throttle(_ block: @escaping () -> Void) { 23 | workItem.cancel() 24 | 25 | workItem = DispatchWorkItem() { 26 | [weak self] in 27 | self?.previousRun = Date() 28 | block() 29 | } 30 | 31 | let deltaDelay = previousRun.timeIntervalSinceNow > delay ? 0 : delay 32 | queue.asyncAfter(deadline: .now() + Double(deltaDelay), execute: workItem) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /viper/VIPER/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /viper/VIPER/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /viper/VIPER/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 | -------------------------------------------------------------------------------- /viper/VIPER/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | --------------------------------------------------------------------------------