├── .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 | 
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 | 
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 | 
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 | 
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 | |  |  |
8 | | --- | --- |
9 | |  |  |
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 |
--------------------------------------------------------------------------------