├── .gitattributes
├── .gitignore
├── .swiftlint.yml
├── Images
├── Coordinators.png
├── iPad_1.png
└── iPad_2.png
├── MVVMC-SplitViewController.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── MVVMC-SplitViewController.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── MVVMC-SplitViewController
├── Application
│ ├── AppCoordinator.swift
│ ├── AppDelegate.swift
│ └── AppDependency.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Extensions
│ ├── ColorCompatibility+Ext.swift
│ └── Observable+Ext.swift
├── Models
│ ├── Address.swift
│ ├── Album.swift
│ ├── Comment.swift
│ ├── Company.swift
│ ├── Geo.swift
│ ├── Photo.swift
│ ├── Post.swift
│ ├── Todo.swift
│ └── User.swift
├── Protocols
│ ├── CoordinatorType.swift
│ ├── ErrorAlertDisplayable.swift
│ ├── PrimaryContainerType.swift
│ ├── Reusable.swift
│ ├── ViewModelAttaching.swift
│ └── ViewModelType.swift
├── Resources
│ └── Assets.xcassets
│ │ ├── AlbumsTabIcon.imageset
│ │ ├── Contents.json
│ │ └── albums.pdf
│ │ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-20.png
│ │ ├── Icon-20@2x-1.png
│ │ ├── Icon-20@2x.png
│ │ ├── Icon-20@3x.png
│ │ ├── Icon-29.png
│ │ ├── Icon-29@2x-1.png
│ │ ├── Icon-29@2x.png
│ │ ├── Icon-29@3x.png
│ │ ├── Icon-40.png
│ │ ├── Icon-40@2x-1.png
│ │ ├── Icon-40@2x.png
│ │ ├── Icon-40@3x.png
│ │ ├── Icon-512@2x.png
│ │ ├── Icon-60@2x.png
│ │ ├── Icon-60@3x.png
│ │ ├── Icon-76.png
│ │ ├── Icon-76@2x.png
│ │ └── Icon-83.5@2x.png
│ │ ├── AquaHaze.colorset
│ │ └── Contents.json
│ │ ├── AthensGray.colorset
│ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── EmptyViewBackground.imageset
│ │ ├── Contents.json
│ │ └── cloud.pdf
│ │ ├── PostsTabIcon.imageset
│ │ ├── Contents.json
│ │ └── today_apps.pdf
│ │ ├── ProfileTabIcon.imageset
│ │ ├── Contents.json
│ │ └── user_male.pdf
│ │ ├── Settings.imageset
│ │ ├── Contents.json
│ │ └── settings.pdf
│ │ ├── StarDust.colorset
│ │ └── Contents.json
│ │ └── TodosTabIcon.imageset
│ │ ├── Contents.json
│ │ └── list.pdf
├── Scenes
│ ├── Albums
│ │ ├── AlbumList
│ │ │ ├── AlbumListViewController.storyboard
│ │ │ ├── AlbumListViewController.swift
│ │ │ └── AlbumListViewModel.swift
│ │ ├── AlbumsCoordinator.swift
│ │ ├── PhotoCollection
│ │ │ ├── PhotoCell.swift
│ │ │ ├── PhotoCellViewModel.swift
│ │ │ ├── PhotoCollectionViewController.storyboard
│ │ │ ├── PhotoCollectionViewController.swift
│ │ │ └── PhotoCollectionViewModel.swift
│ │ └── PhotoDetail
│ │ │ ├── PhotoDetailViewController.storyboard
│ │ │ ├── PhotoDetailViewController.swift
│ │ │ └── PhotoDetailViewModel.swift
│ ├── Common
│ │ ├── ActivityIndicator.swift
│ │ ├── BaseCoordinator.swift
│ │ ├── DetailNavigationController.swift
│ │ ├── DetailNavigationControllerAnimator.swift
│ │ ├── ErrorTracker.swift
│ │ ├── NavigationController.swift
│ │ ├── PlaceholderViewController.swift
│ │ ├── SplitViewCoordinator.swift
│ │ ├── SplitViewDelegate.swift
│ │ ├── TabBarController.swift
│ │ ├── TabBarCoordinator.swift
│ │ └── TableViewController.swift
│ ├── Login
│ │ ├── LoginCoordinator.swift
│ │ ├── LoginViewController.swift
│ │ └── LoginViewModel.swift
│ ├── Posts
│ │ ├── PostDetail
│ │ │ ├── PostDetailViewController.storyboard
│ │ │ ├── PostDetailViewController.swift
│ │ │ └── PostDetailViewModel.swift
│ │ ├── PostsCoordinator.swift
│ │ └── PostsList
│ │ │ ├── PostTableViewCell.swift
│ │ │ ├── PostsListViewController.storyboard
│ │ │ ├── PostsListViewController.swift
│ │ │ └── PostsListViewModel.swift
│ ├── Profile
│ │ ├── ProfileCoordinator.swift
│ │ ├── ProfileViewController.storyboard
│ │ ├── ProfileViewController.swift
│ │ └── ProfileViewModel.swift
│ ├── Settings
│ │ ├── SettingsCoordinator.swift
│ │ ├── SettingsViewController.storyboard
│ │ ├── SettingsViewController.swift
│ │ └── SettingsViewModel.swift
│ ├── Signup
│ │ ├── SignupCoordinator.swift
│ │ ├── SignupViewController.storyboard
│ │ ├── SignupViewController.swift
│ │ └── SignupViewModel.swift
│ └── Todos
│ │ ├── TodosCoordinator.swift
│ │ └── TodosList
│ │ ├── TodosListViewController.storyboard
│ │ ├── TodosListViewController.swift
│ │ └── TodosListViewModel.swift
├── Services
│ ├── APIClient.swift
│ ├── AlbumService.swift
│ ├── PostService.swift
│ ├── Router.swift
│ ├── TodoService.swift
│ └── UserManager.swift
└── Supporting Files
│ └── Info.plist
├── MVVMC-SplitViewControllerTests
├── Info.plist
└── MVVMC_SplitViewControllerTests.swift
├── Podfile
├── Podfile.lock
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj merge=union
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # /Pods
2 |
3 | # OS X Finder
4 | .DS_Store
5 |
6 | ## Build generated
7 | build/
8 | DerivedData/
9 |
10 | # Xcode per-user config
11 | *.mode1
12 | *.mode1v3
13 | *.mode2v3
14 | *.pbxuser
15 | *.perspective
16 | *.perspectivev3
17 | xcuserdata/
18 | # Whitelist defaults since some projects need them
19 | !default.mode1v3
20 | !default.mode2v3
21 | !default.pbxuser
22 | !default.perspectivev3
23 |
24 | # Xcode temporary files
25 | *~.nib
26 |
27 | # Other
28 | *.moved-aside
29 | *.xccheckout
30 |
31 | # CocoaPods
32 | Pods
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules: # rule identifiers to exclude from running
2 | - todo
3 |
4 | excluded: # paths to ignore during linting. Takes precedence over `included`.
5 | - Pods
6 |
7 | # configurable rules can be customized from this configuration file
8 | # binary rules can set their severity level
9 | force_cast: warning # implicitly. Give warning only for force casting
10 |
11 | force_try:
12 | severity: warning # explicitly. Give warning only for force try
13 |
14 | # naming rules can set warnings/errors for min_length and max_length
15 | # additionally they can set excluded names
16 | identifier_name:
17 | excluded:
18 | - bs
19 | - id
20 | - in
21 | - of
22 | - vc
23 |
24 | large_tuple: # warn user when using 3 values in tuple, give error if there are 4
25 | - 3
26 | - 4
27 |
28 | line_length:
29 | # warning: 120
30 | ignores_function_declarations: true
31 | ignores_comments: true
32 |
33 | # naming rules can set warnings/errors for min_length and max_length
34 | # additionally they can set excluded names
35 | type_name:
36 | min_length: 4 # only warning
37 | max_length: # warning and error
38 | warning: 30
39 | error: 35
40 | excluded: iPhone # excluded via string
41 | excluded: in
42 | reporter: "xcode"
--------------------------------------------------------------------------------
/Images/Coordinators.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/Images/Coordinators.png
--------------------------------------------------------------------------------
/Images/iPad_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/Images/iPad_1.png
--------------------------------------------------------------------------------
/Images/iPad_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/Images/iPad_2.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Application/AppCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | class AppCoordinator: BaseCoordinator {
13 | private let window: UIWindow
14 | private let dependencies: AppDependency
15 |
16 | init(window: UIWindow) {
17 | self.window = window
18 | self.dependencies = AppDependency()
19 | }
20 |
21 | override func start() -> Observable {
22 | coordinateToRoot(basedOn: dependencies.userManager.authenticationState)
23 | return .never()
24 | }
25 |
26 | /// Recursive method that will restart a child coordinator after completion.
27 | /// Based on:
28 | /// https://github.com/uptechteam/Coordinator-MVVM-Rx-Example/issues/3
29 | private func coordinateToRoot(basedOn authState: AuthenticationState) {
30 | switch authState {
31 | case .signedIn:
32 | return showSplitView()
33 | .subscribe(onNext: { [weak self] authState in
34 | self?.window.rootViewController = nil
35 | self?.coordinateToRoot(basedOn: authState)
36 | })
37 | .disposed(by: disposeBag)
38 | case .signedOut:
39 | return showLogin()
40 | .subscribe(onNext: { [weak self] authState in
41 | self?.window.rootViewController = nil
42 | self?.coordinateToRoot(basedOn: authState)
43 | })
44 | .disposed(by: disposeBag)
45 | }
46 | }
47 |
48 | private func showSplitView() -> Observable {
49 | let splitViewCoordinator = SplitViewCoordinator(window: self.window, dependencies: dependencies)
50 | return coordinate(to: splitViewCoordinator)
51 | .map { [unowned self] _ in self.dependencies.userManager.authenticationState }
52 | }
53 |
54 | private func showLogin() -> Observable {
55 | let loginCoordinator = LoginCoordinator(window: window, dependencies: dependencies)
56 | return coordinate(to: loginCoordinator)
57 | .map { [unowned self] _ in self.dependencies.userManager.authenticationState }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Application/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/21/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | @UIApplicationMain
13 | class AppDelegate: UIResponder, UIApplicationDelegate {
14 |
15 | var window: UIWindow?
16 | private var appCoordinator: AppCoordinator!
17 | private let disposeBag = DisposeBag()
18 |
19 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
20 |
21 | self.window = UIWindow(frame: UIScreen.main.bounds)
22 |
23 | self.appCoordinator = AppCoordinator(window: self.window!)
24 | self.appCoordinator.start()
25 | .subscribe()
26 | .disposed(by: self.disposeBag)
27 | /*
28 | #if DEBUG
29 | _ = Observable.interval(1, scheduler: MainScheduler.instance)
30 | .subscribe(onNext: { _ in
31 | print("Resource count \(RxSwift.Resources.total)")
32 | })
33 | #endif
34 | */
35 | return true
36 | }
37 |
38 | func applicationWillResignActive(_ application: UIApplication) {
39 | // 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.
40 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
41 | }
42 |
43 | func applicationDidEnterBackground(_ application: UIApplication) {
44 | // 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.
45 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
46 | }
47 |
48 | func applicationWillEnterForeground(_ application: UIApplication) {
49 | // 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.
50 | }
51 |
52 | func applicationDidBecomeActive(_ application: UIApplication) {
53 | // 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.
54 | }
55 |
56 | func applicationWillTerminate(_ application: UIApplication) {
57 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Application/AppDependency.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDependency.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/2/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: - A
12 | // http://merowing.info/2017/04/using-protocol-compositon-for-dependency-injection/
13 | // https://www.swiftbysundell.com/posts/dependency-injection-using-factories-in-swift
14 |
15 | protocol HasClient {
16 | var client: APIClient { get }
17 | }
18 |
19 | protocol HasUserManager {
20 | var userManager: UserManager { get }
21 | }
22 |
23 | protocol HasAlbumService {
24 | var albumService: AlbumServiceType { get }
25 | }
26 |
27 | protocol HasPostService {
28 | var postService: PostServiceType { get }
29 | }
30 |
31 | protocol HasTodoService {
32 | var todoService: TodoServiceType { get }
33 | }
34 |
35 | struct AppDependency: HasClient, HasUserManager, HasAlbumService, HasPostService, HasTodoService {
36 | let client: APIClient
37 | let userManager: UserManager
38 | let albumService: AlbumServiceType
39 | let postService: PostServiceType
40 | let todoService: TodoServiceType
41 |
42 | init() {
43 | self.client = APIClient()
44 | self.userManager = UserManager()
45 | self.albumService = AlbumService(client: client)
46 | self.postService = PostService(client: client)
47 | self.todoService = TodoService(client: client)
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/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 |
27 |
28 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Extensions/ColorCompatibility+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorCompatibility+Ext.swift
3 | // MVVMC-SplitViewController
4 | //
5 |
6 | import UIKit
7 | import ColorCompatibility
8 |
9 | // MARK - RGBA Initializer
10 | extension UIColor {
11 |
12 | /// Creates a color object using the specified opacity and CSS RGB values.
13 | /// - Parameter r: The red value of the color object. Values below 0 are interpreted as 0 and values above 255 are interpreted as 255.
14 | /// - Parameter g: The green value of the color object. Values below 0 are interpreted as 0 and values above 255 are interpreted as 255.
15 | /// - Parameter b: The blue value of the color object. Values below 0 are interpreted as 0 and values above 255 are interpreted as 255.
16 | /// - Parameter a: The opacity value of the color object, specified as a value from 0.0 to 1.0. Alpha values below 0.0 are interpreted as 0.0, and values above 1.0 are interpreted as 1.0.
17 | convenience init(r red: Int, g green: Int, b blue: Int, a alpha: CGFloat) {
18 | self.init(red: UIColor.clampingPercentage(red),
19 | green: UIColor.clampingPercentage(green),
20 | blue: UIColor.clampingPercentage(blue),
21 | alpha: alpha)
22 | }
23 |
24 | fileprivate static func clampingPercentage(_ value: Int) -> CGFloat {
25 | if value <= 0 {
26 | return 0.0
27 | } else if value >= 255 {
28 | return 1.0
29 | } else {
30 | return CGFloat(value) / 255.0
31 | }
32 | }
33 | }
34 |
35 | // See https://noahgilmore.com/blog/dark-mode-uicolor-compatibility/
36 | extension ColorCompatibility {
37 |
38 | // MARK: - Standard Colors
39 |
40 | // MARK: Adaptable Colors
41 |
42 | /// A blue color that automatically adapts to the current trait environment.
43 | static var systemBlue: UIColor {
44 | if #available(iOS 13, *) {
45 | return .systemBlue
46 | }
47 | // Dark
48 | //return UIColor(r: 10, g: 132, b: 255, a: 1.0)
49 | // Light
50 | return UIColor(r: 0, g: 122, b: 255, a: 1.0)
51 | }
52 |
53 | /// A green color that automatically adapts to the current trait environment.
54 | static var systemGreen: UIColor {
55 | if #available(iOS 13, *) {
56 | return .systemGreen
57 | }
58 | // Dark
59 | //return UIColor(r: 48, g: 209, b: 88, a: 1.0)
60 | // Light
61 | return UIColor(r: 52, g: 199, b: 89, a: 1.0)
62 | }
63 |
64 | /// An indigo color that automatically adapts to the current trait environment.
65 | static var systemIndigo: UIColor {
66 | if #available(iOS 13, *) {
67 | return .systemIndigo
68 | }
69 | // Dark
70 | //return UIColor(r: 94, g: 92, b: 230, a: 1.0)
71 | // Light
72 | return UIColor(r: 88, g: 86, b: 214, a: 1.0)
73 | }
74 |
75 | /// An orange color that automatically adapts to the current trait environment.
76 | static var systemOrange: UIColor {
77 | if #available(iOS 13, *) {
78 | return .systemOrange
79 | }
80 | // Dark
81 | //return UIColor(r: 255, g: 159, b: 10, a: 1.0)
82 | // Light
83 | return UIColor(r: 255, g: 149, b: 0, a: 1.0)
84 | }
85 |
86 | /// A pink color that automatically adapts to the current trait environment.
87 | static var systemPink: UIColor {
88 | if #available(iOS 13, *) {
89 | return .systemPink
90 | }
91 | // Dark
92 | //return UIColor(r: 255, g: 55, b: 95, a: 1.0)
93 | // Light
94 | return UIColor(r: 255, g: 45, b: 85, a: 1.0)
95 | }
96 |
97 | /// A purple color that automatically adapts to the current trait environment.
98 | static var systemPurple: UIColor {
99 | if #available(iOS 13, *) {
100 | return .systemPurple
101 | }
102 | // Dark
103 | //return UIColor(r: 191, g: 90, b: 242, a: 1.0)
104 | // Light
105 | return UIColor(r: 175, g: 82, b: 222, a: 1.0)
106 | }
107 |
108 | /// A red color that automatically adapts to the current trait environment.
109 | static var systemRed: UIColor {
110 | if #available(iOS 13, *) {
111 | return .systemRed
112 | }
113 | // Dark
114 | //return UIColor(r: 255, g: 69, b: 58, a: 1.0)
115 | // Light
116 | return UIColor(r: 255, g: 59, b: 48, a: 1.0)
117 | }
118 |
119 | /// A teal color that automatically adapts to the current trait environment.
120 | static var systemTeal: UIColor {
121 | if #available(iOS 13, *) {
122 | return .systemTeal
123 | }
124 | // Dark
125 | //return UIColor(r: 100, g: 210, b: 255, a: 1.0)
126 | // Light
127 | return UIColor(r: 90, g: 200, b: 250, a: 1.0)
128 | }
129 |
130 | /// A yellow color that automatically adapts to the current trait environment.
131 | static var systemYellow: UIColor {
132 | if #available(iOS 13, *) {
133 | return .systemYellow
134 | }
135 | // Dark
136 | //return UIColor(r: 255, g: 214, b: 10, a: 1.0)
137 | // Light
138 | return UIColor(r: 255, g: 204, b: 0, a: 1.0)
139 | }
140 |
141 | }
142 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Extensions/Observable+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Observable+Ext.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 | // From:
9 | // https://github.com/sergdort/CleanArchitectureRxSwift
10 | // Copyright (c) 2017 Sergey Shulga
11 | //
12 |
13 | import RxSwift
14 | import RxCocoa
15 |
16 | extension ObservableType {
17 |
18 | func asDriverOnErrorJustComplete() -> Driver {
19 | return asDriver { _ in
20 | return Driver.empty()
21 | }
22 | }
23 |
24 | func mapToVoid() -> Observable {
25 | return map { _ in }
26 | }
27 | }
28 |
29 | extension PrimitiveSequenceType where Trait == SingleTrait {
30 |
31 | func asDriverOnErrorJustComplete() -> Driver {
32 | return self.primitiveSequence.asDriver { _ in
33 | return Driver.empty()
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Address.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Address.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Address: Codable {
12 | let street: String
13 | let suite: String
14 | let city: String
15 | let zipcode: String
16 | let geo: Geo
17 | }
18 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Album.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Album.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Album: Codable {
12 | let userId: Int
13 | let id: Int
14 | let title: String
15 | }
16 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Comment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comment.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Comment: Codable {
12 | let postId: Int
13 | let id: Int
14 | let name: String
15 | let email: String
16 | let body: String
17 | }
18 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Company.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Company.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Company: Codable {
12 | let name: String
13 | let catchPhrase: String
14 | let bs: String
15 | }
16 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Geo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Geo.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable:next type_name
12 | struct Geo: Codable {
13 | let lat: String
14 | let lng: String
15 | }
16 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Photo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Photo.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Photo: Codable {
12 | let albumId: Int
13 | let id: Int
14 | let title: String
15 | let url: URL
16 | let thumbnailUrl: URL
17 | }
18 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Post.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Post.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Post: Codable {
12 | let userId: Int
13 | let id: Int
14 | let title: String
15 | let body: String
16 | }
17 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/Todo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Todo.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Todo: Codable {
12 | let userId: Int
13 | let id: Int
14 | let title: String
15 | let completed: Bool
16 | }
17 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/26/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct User: Codable {
12 | let id: Int
13 | let name: String
14 | let username: String
15 | let email: String
16 | let address: Address?
17 | let phone: String?
18 | let website: String?
19 | let company: Company?
20 | }
21 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Protocols/CoordinatorType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/23/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | protocol CoordinatorType {
12 | associatedtype CoordinationResult
13 | var identifier: UUID { get }
14 | func start() -> Observable
15 | }
16 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Protocols/ErrorAlertDisplayable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorAlertDisplayable.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 2/27/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 |
12 | protocol ErrorAlertDisplayable {
13 | var errorAlert: Binder { get }
14 | }
15 |
16 | extension ErrorAlertDisplayable where Self: UIViewController {
17 | var errorAlert: Binder {
18 | return Binder(self) { viewController, message in
19 | let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
20 | let action = UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)
21 | alert.addAction(action)
22 | viewController.present(alert, animated: true, completion: nil)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Protocols/PrimaryContainerType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrimaryContainerType.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/28/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | typealias PrimaryContainerType = SplitTabRootViewControllerType & PlaceholderFactory
12 |
13 | /// Represents state of detail view controller in split view controller.
14 | enum DetailView {
15 | case collapsed(UIViewController)
16 | case separated(UIViewController)
17 | case placeholder
18 | }
19 |
20 | protocol SplitTabRootViewControllerType: AnyObject {
21 | /// Called to update secondary view controller with `PlaceholderViewControllerType` when popping view controller.
22 | var detailPopCompletion: (UIViewController & PlaceholderViewControllerType) -> Void { get }
23 |
24 | /// Represents state of detail view controller in split view controller.
25 | var detailView: DetailView { get set }
26 |
27 | /// Add detail view controller to `viewControllers` if it is visible and update `detailView`.
28 | func collapseDetail()
29 |
30 | /// Remove detail view controller from `viewControllers` if it is visible and update `detailView`.
31 | func separateDetail()
32 | }
33 |
34 | extension SplitTabRootViewControllerType where Self: UINavigationController {
35 |
36 | func collapseDetail() {
37 | switch detailView {
38 | case .separated(let detailViewController):
39 | viewControllers += [detailViewController]
40 | detailView = .collapsed(detailViewController)
41 | default:
42 | return
43 | }
44 | }
45 |
46 | func separateDetail() {
47 | switch detailView {
48 | case .collapsed(let detailViewController):
49 | viewControllers.removeLast()
50 | detailView = .separated(detailViewController)
51 | default:
52 | return
53 | }
54 | }
55 |
56 | }
57 |
58 | /// Represents empty detail view controller.
59 | protocol PlaceholderViewControllerType: AnyObject {}
60 |
61 | protocol PlaceholderFactory {
62 | /// Factory method to produce tab-specific placeholder for secondary view controller.
63 | func makePlaceholderViewController() -> UIViewController & PlaceholderViewControllerType
64 | }
65 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Protocols/Reusable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reusable.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 | // Based on code from:
9 | // https://cocoacasts.com/dequeueing-reusable-views-with-generics-and-protocols
10 | // https://github.com/sergdort/CleanArchitectureRxSwift
11 | //
12 |
13 | import UIKit
14 |
15 | protocol Reusable {
16 | static var reuseID: String {get}
17 | }
18 |
19 | extension Reusable {
20 | static var reuseID: String {
21 | return String(describing: self)
22 | }
23 | }
24 |
25 | // MARK: - View Controller
26 |
27 | extension UIViewController: Reusable {
28 | class func instance() -> Self {
29 | let storyboard = UIStoryboard(name: reuseID, bundle: nil)
30 | return storyboard.instantiateViewController()
31 | }
32 | }
33 |
34 | extension UIStoryboard {
35 | func instantiateViewController() -> T {
36 | guard let viewController = self.instantiateViewController(withIdentifier: T.reuseID) as? T else {
37 | fatalError("Unable to instantiate view controller: \(T.self)")
38 | }
39 | return viewController
40 | }
41 | }
42 |
43 | // MARK: - Collection View
44 |
45 | extension UICollectionReusableView: Reusable {}
46 |
47 | extension UICollectionView {
48 |
49 | func dequeueReusableCell(for indexPath: IndexPath) -> T {
50 | guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseID, for: indexPath) as? T else {
51 | fatalError("Unable to dequeue reusable collection view cell: \(T.self)")
52 | }
53 | return cell
54 | }
55 |
56 | func dequeueReusableSupplementaryView(ofKind kind: String, for indexPath: IndexPath) -> T {
57 | guard let section = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.reuseID, for: indexPath) as? T else {
58 | fatalError("Unable to dequeue reusable supplementary view: \(T.self)")
59 | }
60 | return section
61 | }
62 |
63 | }
64 |
65 | // MARK: - Table View
66 |
67 | extension UITableViewCell: Reusable {}
68 |
69 | extension UITableView {
70 | func dequeueReusableCell(for indexPath: IndexPath) -> T {
71 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseID, for: indexPath) as? T else {
72 | fatalError("Unable to dequeue reusable table view cell: \(T.self)")
73 | }
74 | return cell
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Protocols/ViewModelAttaching.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModelAttaching.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | protocol ViewModelAttaching: AnyObject {
13 | associatedtype ViewModel: ViewModelType
14 |
15 | var bindings: ViewModel.Bindings { get }
16 | var viewModel: Attachable! { get set }
17 |
18 | func attach(wrapper: Attachable) -> ViewModel
19 | func bind(viewModel: ViewModel) -> ViewModel
20 | }
21 |
22 | extension ViewModelAttaching where Self: UIViewController {
23 |
24 | @discardableResult
25 | func attach(wrapper: Attachable) -> ViewModel {
26 | viewModel = wrapper
27 | loadViewIfNeeded()
28 | let vm = viewModel.bind(bindings)
29 | return bind(viewModel: vm)
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Protocols/ViewModelType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModelType.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 | // Adopted from Thomas Visser:
9 | // [Reactive MVVM](http://www.thomasvisser.me/2017/02/09/mvvm-rx/)
10 | //
11 |
12 | import Foundation
13 |
14 | protocol ViewModelType {
15 | associatedtype Dependency
16 | associatedtype Bindings
17 |
18 | init(dependency: Dependency, bindings: Bindings)
19 | }
20 |
21 | enum Attachable {
22 |
23 | case detached(VM.Dependency)
24 | case attached(VM.Dependency, VM)
25 |
26 | mutating func bind(_ bindings: VM.Bindings) -> VM {
27 | switch self {
28 | case let .detached(dependency):
29 | let vm = VM(dependency: dependency, bindings: bindings)
30 | self = .attached(dependency, vm)
31 | return vm
32 | case let .attached(dependency, _):
33 | let vm = VM(dependency: dependency, bindings: bindings)
34 | self = .attached(dependency, vm)
35 | return vm
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AlbumsTabIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "albums.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template",
14 | "preserves-vector-representation" : true
15 | }
16 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AlbumsTabIcon.imageset/albums.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AlbumsTabIcon.imageset/albums.pdf
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-20@2x-1.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-29@2x-1.png",
19 | "scale" : "2x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-29@3x.png",
25 | "scale" : "3x"
26 | },
27 | {
28 | "size" : "40x40",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-40@2x-1.png",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-40@3x.png",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-60@2x.png",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-60@3x.png",
49 | "scale" : "3x"
50 | },
51 | {
52 | "size" : "20x20",
53 | "idiom" : "ipad",
54 | "filename" : "Icon-20.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-20@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "29x29",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-29.png",
67 | "scale" : "1x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-29@2x.png",
73 | "scale" : "2x"
74 | },
75 | {
76 | "size" : "40x40",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-40.png",
79 | "scale" : "1x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-40@2x.png",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "76x76",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-76.png",
91 | "scale" : "1x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-76@2x.png",
97 | "scale" : "2x"
98 | },
99 | {
100 | "size" : "83.5x83.5",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-83.5@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "1024x1024",
107 | "idiom" : "ios-marketing",
108 | "filename" : "Icon-512@2x.png",
109 | "scale" : "1x"
110 | }
111 | ],
112 | "info" : {
113 | "version" : 1,
114 | "author" : "xcode"
115 | }
116 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512@2x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AquaHaze.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.940",
13 | "alpha" : "1.000",
14 | "blue" : "0.960",
15 | "green" : "0.950"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/AthensGray.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.940",
13 | "alpha" : "1.000",
14 | "blue" : "0.960",
15 | "green" : "0.940"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/EmptyViewBackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "cloud.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template",
14 | "preserves-vector-representation" : true
15 | }
16 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/EmptyViewBackground.imageset/cloud.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/EmptyViewBackground.imageset/cloud.pdf
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/PostsTabIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "today_apps.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template",
14 | "preserves-vector-representation" : true
15 | }
16 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/PostsTabIcon.imageset/today_apps.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/PostsTabIcon.imageset/today_apps.pdf
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/ProfileTabIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "user_male.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template",
14 | "preserves-vector-representation" : true
15 | }
16 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/ProfileTabIcon.imageset/user_male.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/ProfileTabIcon.imageset/user_male.pdf
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/Settings.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "settings.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template",
14 | "preserves-vector-representation" : true
15 | }
16 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/Settings.imageset/settings.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/Settings.imageset/settings.pdf
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/StarDust.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.630",
13 | "alpha" : "1.000",
14 | "blue" : "0.630",
15 | "green" : "0.630"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/TodosTabIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "list.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template",
14 | "preserves-vector-representation" : true
15 | }
16 | }
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Resources/Assets.xcassets/TodosTabIcon.imageset/list.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mgacy/MVVMC-SplitViewController/001b166518229db0c3a05f7f6964d78845ae78bf/MVVMC-SplitViewController/Resources/Assets.xcassets/TodosTabIcon.imageset/list.pdf
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/AlbumList/AlbumListViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/AlbumList/AlbumListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumListViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/9/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | final class AlbumListViewController: TableViewController, ViewModelAttaching {
14 |
15 | var viewModel: Attachable!
16 | var bindings: AlbumListViewModel.Bindings {
17 | let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
18 | .mapToVoid()
19 | .asDriverOnErrorJustComplete()
20 | let refresh = tableView.refreshControl!.rx
21 | .controlEvent(.valueChanged)
22 | .asDriver()
23 |
24 | return AlbumListViewModel.Bindings(
25 | fetchTrigger: Driver.merge(viewWillAppear, refresh),
26 | selection: tableView.rx.itemSelected.asDriver()
27 | )
28 | }
29 |
30 | // MARK: - Lifecycle
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | setupView()
35 | }
36 |
37 | // MARK: - View Methods
38 |
39 | private func setupView() {
40 | title = "Albums"
41 | }
42 |
43 | func bind(viewModel: AlbumListViewModel) -> AlbumListViewModel {
44 | viewModel.albums
45 | .drive(tableView.rx.items(cellIdentifier: "Cell")) { _, element, cell in
46 | cell.textLabel?.text = element.title
47 | }
48 | .disposed(by: disposeBag)
49 |
50 | viewModel.fetching
51 | .drive(tableView.refreshControl!.rx.isRefreshing)
52 | .disposed(by: disposeBag)
53 |
54 | viewModel.errors
55 | .delay(.milliseconds(100))
56 | .map { $0.localizedDescription }
57 | .drive(errorAlert)
58 | .disposed(by: disposeBag)
59 |
60 | return viewModel
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/AlbumList/AlbumListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumListViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/9/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | final class AlbumListViewModel: ViewModelType {
13 |
14 | let fetching: Driver
15 | let albums: Driver<[Album]>
16 | let selectedAlbum: Driver
17 | let errors: Driver
18 |
19 | // MARK: - Lifecycle
20 |
21 | init(dependency: Dependency, bindings: Bindings) {
22 | let activityIndicator = ActivityIndicator()
23 | let errorTracker = ErrorTracker()
24 |
25 | albums = bindings.fetchTrigger
26 | .flatMapLatest {
27 | return dependency.albumService.getAlbums()
28 | .trackActivity(activityIndicator)
29 | .trackError(errorTracker)
30 | .asDriverOnErrorJustComplete()
31 | }
32 |
33 | fetching = activityIndicator.asDriver()
34 | errors = errorTracker.asDriver()
35 | selectedAlbum = bindings.selection
36 | .withLatestFrom(self.albums) { (indexPath, albums: [Album]) -> Album in
37 | return albums[indexPath.row]
38 | }
39 | }
40 |
41 | // MARK: - ViewModelType
42 |
43 | typealias Dependency = HasAlbumService
44 |
45 | struct Bindings {
46 | let fetchTrigger: Driver
47 | let selection: Driver
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/AlbumsCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumsCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/9/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | class AlbumsCoordinator: BaseCoordinator {
12 | typealias Dependencies = HasAlbumService
13 |
14 | private let navigationController: UINavigationController
15 | private let dependencies: Dependencies
16 |
17 | init(navigationController: UINavigationController, dependencies: Dependencies) {
18 | self.navigationController = navigationController
19 | self.dependencies = dependencies
20 | }
21 |
22 | override func start() -> Observable {
23 | let viewController = AlbumListViewController.instance()
24 | navigationController.viewControllers = [viewController]
25 |
26 | let avm: Attachable = .detached(dependencies)
27 | let viewModel = viewController.attach(wrapper: avm)
28 |
29 | viewModel.selectedAlbum
30 | .drive(onNext: { [weak self] selection in
31 | self?.showDetailView(with: selection)
32 | })
33 | .disposed(by: viewController.disposeBag)
34 |
35 | // View will never be dismissed
36 | return Observable.never()
37 | }
38 |
39 | private func showDetailView(with album: Album) {
40 | let viewController = PhotoCollectionViewController.instance()
41 | let avm: Attachable = .detached(PhotoCollectionViewModel.Dependency(
42 | albumService: dependencies.albumService, album: album))
43 | let viewModel = viewController.attach(wrapper: avm)
44 |
45 | navigationController.pushViewController(viewController, animated: true)
46 |
47 | viewModel.selectedPhoto
48 | .drive(onNext: { [weak self] photoCellViewModel in
49 | self?.showDetail(with: photoCellViewModel.photo)
50 | })
51 | .disposed(by: viewController.disposeBag)
52 | }
53 |
54 | private func showDetail(with photo: Photo) {
55 | let viewController = PhotoDetailViewController.instance()
56 | viewController.viewModel = PhotoDetailViewModel(albumService: dependencies.albumService, photo: photo)
57 | navigationController.showDetailViewController(viewController, sender: nil)
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/PhotoCollection/PhotoCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCell.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/11/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | class PhotoCell: UICollectionViewCell {
13 | @IBOutlet weak var imageView: UIImageView!
14 | @IBOutlet weak var titleLabel: UILabel!
15 |
16 | private(set) var disposeBag = DisposeBag()
17 |
18 | override func awakeFromNib() {
19 | super.awakeFromNib()
20 | imageView.layer.cornerRadius = 5
21 | imageView.layer.masksToBounds = true
22 | }
23 |
24 | override func prepareForReuse() {
25 | super.prepareForReuse()
26 | disposeBag = DisposeBag() // because life cicle of every cell ends on prepare for reuse
27 | }
28 |
29 | func bind(to viewModel: PhotoCellViewModel) {
30 | viewModel.title
31 | .drive(titleLabel.rx.text)
32 | .disposed(by: disposeBag)
33 |
34 | viewModel.thumbnail
35 | .drive(imageView.rx.image)
36 | .disposed(by: disposeBag)
37 | }
38 |
39 | }
40 |
41 | // MARK: - SectionView
42 |
43 | class PhotoSectionView: UICollectionReusableView {
44 | @IBOutlet weak var titleLabel: UILabel?
45 | }
46 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/PhotoCollection/PhotoCellViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCellViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/11/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Differentiator
10 | import RxCocoa
11 | import RxSwift
12 |
13 | final class PhotoCellViewModel {
14 |
15 | let title: Driver
16 | let thumbnail: Driver
17 | let photo: Photo
18 |
19 | init(albumService: AlbumServiceType, photo: Photo) {
20 | self.photo = photo
21 | self.title = Driver.just(photo.title)
22 | self.thumbnail = albumService.getThumbnail(for: photo)
23 | .asDriver(onErrorDriveWith: .empty())
24 | }
25 |
26 | }
27 |
28 | // MARK: - RxDataSources - AnimatableSectionModelType
29 |
30 | extension PhotoCellViewModel: IdentifiableType {
31 | typealias Identity = Int
32 |
33 | var identity: Int {
34 | return photo.id
35 | }
36 | }
37 |
38 | extension PhotoCellViewModel: Equatable {
39 |
40 | static func == (lhs: PhotoCellViewModel, rhs: PhotoCellViewModel) -> Bool {
41 | return lhs.photo.id == rhs.photo.id
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/PhotoCollection/PhotoCollectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCollectionViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/9/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxDataSources
12 | import RxSwift
13 |
14 | class PhotoCollectionViewController: UIViewController, ViewModelAttaching {
15 |
16 | let disposeBag = DisposeBag()
17 |
18 | var viewModel: Attachable!
19 | var bindings: PhotoCollectionViewModel.Bindings {
20 | return PhotoCollectionViewModel.Bindings(
21 | selection: collectionView.rx.itemSelected.asDriver()
22 | )
23 | }
24 |
25 | //fileprivate let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)
26 |
27 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
28 | @IBOutlet weak var collectionView: UICollectionView!
29 |
30 | // MARK: - Lifecycle
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | setupView()
35 | }
36 |
37 | // MARK: - View Methods
38 |
39 | private func setupView() {
40 | title = "Photos"
41 | }
42 |
43 | func bind(viewModel: PhotoCollectionViewModel) -> PhotoCollectionViewModel {
44 | let (configureCollectionViewCell, configureSupplementaryView) = PhotoCollectionViewController.collectionViewDataSourceUI()
45 | let cvDataSource = RxCollectionViewSectionedAnimatedDataSource(
46 | configureCell: configureCollectionViewCell,
47 | configureSupplementaryView: configureSupplementaryView
48 | )
49 |
50 | viewModel.photos
51 | .asObservable()
52 | .bind(to: collectionView.rx.items(dataSource: cvDataSource))
53 | .disposed(by: disposeBag)
54 |
55 | viewModel.fetching
56 | .drive(activityIndicator.rx.isAnimating)
57 | .disposed(by: disposeBag)
58 |
59 | viewModel.fetching
60 | .drive(collectionView.rx.isHidden)
61 | .disposed(by: disposeBag)
62 |
63 | return viewModel
64 | }
65 |
66 | }
67 |
68 | extension PhotoCollectionViewController {
69 |
70 | static func collectionViewDataSourceUI() -> (CollectionViewSectionedDataSource.ConfigureCell, CollectionViewSectionedDataSource.ConfigureSupplementaryView) {
71 | return (
72 | // swiftlint:disable:next identifier_name
73 | { (_, cv, ip, i) in
74 | let cell: PhotoCell = cv.dequeueReusableCell(for: ip)
75 | cell.bind(to: i)
76 | return cell
77 | },
78 | // swiftlint:disable:next identifier_name
79 | { (ds, cv, kind, ip) in
80 | let section: PhotoSectionView = cv.dequeueReusableSupplementaryView(ofKind: kind, for: ip)
81 | section.titleLabel!.text = "\(ds[ip.section].header)"
82 | return section
83 | }
84 | )
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/PhotoCollection/PhotoCollectionViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCollectionViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/9/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Differentiator
10 | import RxDataSources
11 | import RxCocoa
12 | import RxSwift
13 |
14 | final class PhotoCollectionViewModel: ViewModelType {
15 |
16 | let fetching: Driver
17 | //let albumTitle: Driver
18 | let photos: Driver<[PhotoSection]>
19 | let selectedPhoto: Driver
20 | let errors: Driver
21 |
22 | // MARK: - Lifecycle
23 |
24 | init(dependency: Dependency, bindings: Bindings) {
25 | let activityIndicator = ActivityIndicator()
26 | let errorTracker = ErrorTracker()
27 |
28 | photos = dependency.albumService.getPhotos()
29 | .trackActivity(activityIndicator)
30 | .map { photo in
31 | return photo.map {
32 | return PhotoCellViewModel.init(albumService: dependency.albumService, photo: $0)
33 | }
34 | }
35 | .map {
36 | return [PhotoSection(header: dependency.album.title, photos: $0)]
37 | }
38 | .trackError(errorTracker)
39 | .asDriverOnErrorJustComplete()
40 |
41 | fetching = activityIndicator.asDriver()
42 | errors = errorTracker.asDriver()
43 |
44 | selectedPhoto = bindings.selection
45 | .withLatestFrom(self.photos) { (indexPath, sections: [PhotoSection]) -> PhotoCellViewModel in
46 | return sections[indexPath.section].items[indexPath.row]
47 | }
48 | }
49 |
50 | // MARK: - ViewModelType
51 |
52 | struct Dependency {
53 | let albumService: AlbumServiceType
54 | let album: Album
55 | }
56 |
57 | struct Bindings {
58 | let selection: Driver
59 | }
60 |
61 | }
62 |
63 | // MARK: - DataSource
64 |
65 | struct PhotoSection {
66 | var header: String
67 | var photos: [PhotoCellViewModel]
68 |
69 | init(header: String, photos: [Item]) {
70 | self.header = header
71 | self.photos = photos
72 | }
73 |
74 | }
75 |
76 | extension PhotoSection: AnimatableSectionModelType {
77 | typealias Item = PhotoCellViewModel
78 | typealias Identity = String
79 |
80 | var identity: String {
81 | return header
82 | }
83 |
84 | var items: [PhotoCellViewModel] {
85 | return photos
86 | }
87 |
88 | init(original: PhotoSection, items: [PhotoCellViewModel]) {
89 | self = original
90 | self.photos = items
91 | }
92 |
93 | }
94 |
95 | extension PhotoSection: Equatable {
96 |
97 | static func == (lhs: PhotoSection, rhs: PhotoSection) -> Bool {
98 | return lhs.header == rhs.header && lhs.items == rhs.items
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/PhotoDetail/PhotoDetailViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/PhotoDetail/PhotoDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoDetailViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/11/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | final class PhotoDetailViewController: UIViewController {
14 |
15 | let disposeBag = DisposeBag()
16 | var viewModel: PhotoDetailViewModel!
17 |
18 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
19 | @IBOutlet weak var imageView: UIImageView!
20 |
21 | // MARK: - Lifecycle
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 | setupView()
26 | }
27 |
28 | // MARK: - View Methods
29 |
30 | private func setupView() {
31 | bindViewModel()
32 | }
33 |
34 | private func bindViewModel() {
35 | viewModel.title
36 | .drive(self.rx.title)
37 | .disposed(by: disposeBag)
38 |
39 | viewModel.image
40 | .drive(imageView.rx.image)
41 | .disposed(by: disposeBag)
42 |
43 | viewModel.fetching
44 | .drive(activityIndicator.rx.isAnimating)
45 | .disposed(by: disposeBag)
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Albums/PhotoDetail/PhotoDetailViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoDetailViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 2/22/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | final class PhotoDetailViewModel {
13 |
14 | let fetching: Driver
15 | let errors: Driver
16 | let title: Driver
17 | let image: Driver
18 |
19 | private let photo: Photo
20 |
21 | init(albumService: AlbumServiceType, photo: Photo) {
22 | self.photo = photo
23 |
24 | let activityIndicator = ActivityIndicator()
25 | let errorTracker = ErrorTracker()
26 |
27 | self.title = Driver.just(photo.title)
28 |
29 | self.image = albumService.getImage(for: photo)
30 | .trackActivity(activityIndicator)
31 | .trackError(errorTracker)
32 | .asDriverOnErrorJustComplete()
33 |
34 | fetching = activityIndicator.asDriver()
35 | errors = errorTracker.asDriver()
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/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 | // From:
9 | // https://github.com/ReactiveX/RxSwift
10 | //
11 |
12 | import RxSwift
13 | import RxCocoa
14 |
15 | // swiftlint:disable identifier_name
16 | // swiftlint:disable multiple_closures_with_trailing_closure
17 | // swiftlint:disable type_name
18 |
19 | private struct ActivityToken : ObservableConvertibleType, Disposable {
20 | private let _source: Observable
21 | private let _dispose: Cancelable
22 |
23 | init(source: Observable, disposeAction: @escaping () -> Void) {
24 | _source = source
25 | _dispose = Disposables.create(with: disposeAction)
26 | }
27 |
28 | func dispose() {
29 | _dispose.dispose()
30 | }
31 |
32 | func asObservable() -> Observable {
33 | return _source
34 | }
35 | }
36 |
37 | /**
38 | Enables monitoring of sequence computation.
39 | If there is at least one sequence computation in progress, `true` will be sent.
40 | When all activities complete `false` will be sent.
41 | */
42 | public class ActivityIndicator : SharedSequenceConvertibleType {
43 | public typealias Element = Bool
44 | public typealias SharingStrategy = DriverSharingStrategy
45 |
46 | private let _lock = NSRecursiveLock()
47 | private let _relay = BehaviorRelay(value: 0)
48 | private let _loading: SharedSequence
49 |
50 | public init() {
51 | _loading = _relay.asDriver()
52 | .map { $0 > 0 }
53 | .distinctUntilChanged()
54 | }
55 |
56 | fileprivate func trackActivityOfObservable(_ source: Source) -> Observable {
57 | return Observable.using({ () -> ActivityToken in
58 | self.increment()
59 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
60 | }) { t in
61 | return t.asObservable()
62 | }
63 | }
64 |
65 | private func increment() {
66 | _lock.lock()
67 | _relay.accept(_relay.value + 1)
68 | _lock.unlock()
69 | }
70 |
71 | private func decrement() {
72 | _lock.lock()
73 | _relay.accept(_relay.value - 1)
74 | _lock.unlock()
75 | }
76 |
77 | public func asSharedSequence() -> SharedSequence {
78 | return _loading
79 | }
80 | }
81 |
82 | extension ObservableConvertibleType {
83 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable {
84 | return activityIndicator.trackActivityOfObservable(self)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/BaseCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 | // From:
9 | // https://github.com/uptechteam/Coordinator-MVVM-Rx-Example
10 | // Arthur Myronenko
11 | // Copyright © 2017 UPTech Team.
12 | //
13 |
14 | import RxSwift
15 |
16 | /// Base abstract coordinator generic over the return type of the `start` method.
17 | class BaseCoordinator: CoordinatorType {
18 |
19 | /// Typealias which allows to access a ResultType of the Coordainator by `CoordinatorName.CoordinationResult`.
20 | typealias CoordinationResult = ResultType
21 |
22 | /// Utility `DisposeBag` used by the subclasses.
23 | let disposeBag = DisposeBag()
24 |
25 | /// Unique identifier.
26 | internal let identifier = UUID()
27 |
28 | /// Dictionary of the child coordinators. Every child coordinator should be added
29 | /// to that dictionary in order to keep it in memory.
30 | /// Key is an `identifier` of the child coordinator and value is the coordinator itself.
31 | /// Value type is `Any` because Swift doesn't allow to store generic types in the array.
32 | private var childCoordinators = [UUID: Any]()
33 |
34 | /// Stores coordinator to the `childCoordinators` dictionary.
35 | ///
36 | /// - Parameter coordinator: Child coordinator to store.
37 | private func store(coordinator: T) {
38 | childCoordinators[coordinator.identifier] = coordinator
39 | }
40 |
41 | /// Release coordinator from the `childCoordinators` dictionary.
42 | ///
43 | /// - Parameter coordinator: Coordinator to release.
44 | private func free(coordinator: T) {
45 | childCoordinators[coordinator.identifier] = nil
46 | }
47 |
48 | /// 1. Stores coordinator in a dictionary of child coordinators.
49 | /// 2. Calls method `start()` on that coordinator.
50 | /// 3. On the `onNext:` of returning observable of method `start()` removes coordinator from the dictionary.
51 | ///
52 | /// - Parameter coordinator: Coordinator to start.
53 | /// - Returns: Result of `start()` method.
54 | func coordinate(to coordinator: T) -> Observable where U == T.CoordinationResult {
55 | store(coordinator: coordinator)
56 | return coordinator.start()
57 | .do(onNext: { [weak self] _ in self?.free(coordinator: coordinator) })
58 | }
59 |
60 | /// Starts job of the coordinator.
61 | ///
62 | /// - Returns: Result of coordinator job.
63 | func start() -> Observable {
64 | fatalError("Start method should be implemented.")
65 | }
66 | }
67 |
68 | // MARK: - CustomStringConvertible
69 | extension BaseCoordinator: CustomStringConvertible {
70 | var description: String {
71 | return "\(type(of: self))"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/DetailNavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailNavigationController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/29/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class DetailNavigationController: UINavigationController {
12 |
13 | init() {
14 | super.init(nibName: nil, bundle: nil)
15 | delegate = self
16 | }
17 |
18 | required init?(coder aDecoder: NSCoder) {
19 | fatalError("init(coder:) has not been implemented")
20 | }
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | navigationBar.isTranslucent = false
25 | }
26 |
27 | }
28 |
29 | // MARK: - UINavigationControllerDelegate
30 | extension DetailNavigationController: UINavigationControllerDelegate {
31 |
32 | public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
33 | guard operation == .push, toVC is PlaceholderViewController else {
34 | return nil
35 | }
36 |
37 | return DetailNavigationControllerAnimator(operation: operation)
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/DetailNavigationControllerAnimator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailNavigationControllerAnimator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 2/22/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class DetailNavigationControllerAnimator: NSObject, UIViewControllerAnimatedTransitioning {
12 | let operation: UINavigationController.Operation
13 |
14 | init(operation: UINavigationController.Operation) {
15 | self.operation = operation
16 | super.init()
17 | }
18 |
19 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
20 | return 0.35
21 | }
22 |
23 | public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
24 | guard
25 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
26 | let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
27 | return
28 | }
29 |
30 | if operation == .push {
31 | switch toVC is PlaceholderViewControllerType {
32 | case true:
33 | animatePushAsPop(from: fromVC, to: toVC, using: transitionContext)
34 | case false:
35 | animatePush(from: fromVC, to: toVC, using: transitionContext)
36 | }
37 | } else if operation == .pop {
38 | animatePop(from: fromVC, to: toVC, using: transitionContext)
39 | }
40 | }
41 |
42 | // MARK: - Push / Pop
43 |
44 | private func animatePush(from fromVC: UIViewController, to toVC: UIViewController, using transitionContext: UIViewControllerContextTransitioning) {
45 | let containerView = transitionContext.containerView
46 | let finalFrame = transitionContext.finalFrame(for: toVC)
47 |
48 | let dx = containerView.frame.size.width
49 | toVC.view.frame = finalFrame.offsetBy(dx: dx, dy: 0.0)
50 | containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
51 |
52 | UIView.animate(
53 | withDuration: transitionDuration(using: transitionContext), delay: 0,
54 | options: [ UIView.AnimationOptions.curveEaseOut ],
55 | animations: {
56 | toVC.view.frame = transitionContext.finalFrame(for: toVC)
57 | fromVC.view.frame = finalFrame.offsetBy(dx: dx / -2.5, dy: 0.0)
58 | },
59 | completion: { (finished) in transitionContext.completeTransition(true) }
60 | )
61 | }
62 |
63 | private func animatePushAsPop(from fromVC: UIViewController, to toVC: UIViewController, using transitionContext: UIViewControllerContextTransitioning) {
64 | let containerView = transitionContext.containerView
65 | let finalFrame = transitionContext.finalFrame(for: toVC)
66 |
67 | let dx = containerView.frame.size.width
68 | toVC.view.frame = finalFrame.offsetBy(dx: dx / -2.5, dy: 0.0)
69 | containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
70 |
71 | UIView.animate(
72 | withDuration: transitionDuration(using: transitionContext), delay: 0,
73 | options: [ UIView.AnimationOptions.curveEaseOut ],
74 | animations: {
75 | toVC.view.frame = transitionContext.finalFrame(for: toVC)
76 | fromVC.view.frame = finalFrame.offsetBy(dx: dx, dy: 0.0)
77 | },
78 | completion: { (finished) in transitionContext.completeTransition(true) }
79 | )
80 | }
81 |
82 | private func animatePop(from fromVC: UIViewController, to toVC: UIViewController, using transitionContext: UIViewControllerContextTransitioning) {
83 | let containerView = transitionContext.containerView
84 | containerView.addSubview(toVC.view)
85 |
86 | UIView.animate(
87 | withDuration: transitionDuration(using: transitionContext), delay: 0,
88 | options: [ UIView.AnimationOptions.curveEaseOut ],
89 | animations: {
90 | fromVC.view.frame = containerView.bounds.offsetBy(dx: containerView.frame.width, dy: 0)
91 | toVC.view.frame = containerView.bounds
92 | },
93 | completion: { (finished) in transitionContext.completeTransition(true) }
94 | )
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/ErrorTracker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorTracker.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 | // From:
9 | // https://github.com/sergdort/CleanArchitectureRxSwift
10 | // Copyright © 2017 Sergey Shulga. All rights reserved.
11 | //
12 |
13 | import RxSwift
14 | import RxCocoa
15 |
16 | final class ErrorTracker: SharedSequenceConvertibleType {
17 | typealias SharingStrategy = DriverSharingStrategy
18 | private let _subject = PublishSubject()
19 |
20 | func trackError(from source: O) -> Observable {
21 | return source.asObservable().do(onError: onError)
22 | }
23 |
24 | func asSharedSequence() -> SharedSequence {
25 | return _subject.asObservable().asDriverOnErrorJustComplete()
26 | }
27 |
28 | func asObservable() -> Observable {
29 | return _subject.asObservable()
30 | }
31 |
32 | private func onError(_ error: Error) {
33 | _subject.onNext(error)
34 | }
35 |
36 | deinit {
37 | _subject.onCompleted()
38 | }
39 | }
40 |
41 | extension ObservableConvertibleType {
42 | func trackError(_ errorTracker: ErrorTracker) -> Observable {
43 | return errorTracker.trackError(from: self)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/NavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/8/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NavigationController: UINavigationController, PrimaryContainerType {
12 |
13 | let detailPopCompletion: (UIViewController & PlaceholderViewControllerType) -> Void
14 | var detailView: DetailView = .placeholder
15 |
16 | // MARK: - Lifecycle
17 |
18 | init(withPopDetailCompletion completion: @escaping (UIViewController & PlaceholderViewControllerType) -> Void) {
19 | self.detailPopCompletion = completion
20 | super.init(nibName: nil, bundle: nil)
21 | }
22 |
23 | required init?(coder aDecoder: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | override func viewDidLoad() {
28 | super.viewDidLoad()
29 | navigationBar.isTranslucent = false
30 | }
31 |
32 | override func popViewController(animated: Bool) -> UIViewController? {
33 | switch detailView {
34 | case .collapsed:
35 | detailView = .placeholder
36 | case .separated:
37 | detailView = .placeholder
38 | /// Set detail view controller to `PlaceholderViewControllerType` to prevent confusion
39 | detailPopCompletion(makePlaceholderViewController())
40 | case .placeholder:
41 | break
42 | }
43 | return super.popViewController(animated: animated)
44 | }
45 |
46 | func makePlaceholderViewController() -> UIViewController & PlaceholderViewControllerType {
47 | return PlaceholderViewController()
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/PlaceholderViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaceholderViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/8/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import ColorCompatibility
11 |
12 | class PlaceholderViewController: UIViewController, PlaceholderViewControllerType {
13 |
14 | let backgroundImageView: UIImageView = {
15 | let view = UIImageView()
16 | view.image = #imageLiteral(resourceName: "EmptyViewBackground")
17 | view.tintColor = ColorCompatibility.systemGray2
18 | view.translatesAutoresizingMaskIntoConstraints = false
19 | return view
20 | }()
21 |
22 | // MARK: - Lifecycle
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 | setupView()
27 | setupConstraints()
28 | }
29 |
30 | override func viewWillAppear(_ animated: Bool) {
31 | if let displayModeButtonItem = splitViewController?.displayModeButtonItem {
32 | navigationItem.leftBarButtonItem = displayModeButtonItem
33 | }
34 | }
35 |
36 | // MARK: - View Methods
37 |
38 | func setupView() {
39 | view.backgroundColor = ColorCompatibility.systemBackground
40 | self.view.addSubview(backgroundImageView)
41 | navigationItem.leftItemsSupplementBackButton = true
42 | }
43 |
44 | func setupConstraints() {
45 | NSLayoutConstraint.activate([
46 | backgroundImageView.widthAnchor.constraint(equalToConstant: 180.0),
47 | backgroundImageView.heightAnchor.constraint(equalToConstant: 180.0),
48 | backgroundImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
49 | backgroundImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
50 | ])
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/SplitViewCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SplitViewCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | class SplitViewCoordinator: BaseCoordinator {
12 | typealias Dependencies = HasClient & HasUserManager & HasAlbumService & HasPostService & HasTodoService
13 |
14 | private let window: UIWindow
15 | private let dependencies: Dependencies
16 |
17 | // swiftlint:disable:next weak_delegate
18 | private let viewDelegate: SplitViewDelegate
19 |
20 | enum SectionTab {
21 | case posts
22 | case albums
23 | case todos
24 | case profile
25 |
26 | var title: String {
27 | switch self {
28 | case .posts: return "Posts"
29 | case .albums: return "Albums"
30 | case .todos: return "Todos"
31 | case .profile: return "Profile"
32 | }
33 | }
34 |
35 | var image: UIImage {
36 | switch self {
37 | case .posts: return #imageLiteral(resourceName: "PostsTabIcon")
38 | case .albums: return #imageLiteral(resourceName: "AlbumsTabIcon")
39 | case .todos: return #imageLiteral(resourceName: "TodosTabIcon")
40 | case .profile: return #imageLiteral(resourceName: "ProfileTabIcon")
41 | }
42 | }
43 |
44 | }
45 |
46 | // MARK: - Lifecycle
47 |
48 | init(window: UIWindow, dependencies: Dependencies) {
49 | self.window = window
50 | self.dependencies = dependencies
51 |
52 | let detailNavigationController = DetailNavigationController()
53 | self.viewDelegate = SplitViewDelegate(detailNavigationController: detailNavigationController)
54 | }
55 |
56 | override func start() -> Observable {
57 | let tabBarController = UITabBarController()
58 | let tabs: [SectionTab] = [.posts, .albums, .todos, .profile]
59 | let coordinationResults = Observable.from(configure(tabBarController: tabBarController, withTabs: tabs)).merge()
60 |
61 | if let initialPrimaryView = tabBarController.selectedViewController as? PrimaryContainerType {
62 | viewDelegate.updateSecondaryWithDetail(from: initialPrimaryView)
63 | }
64 |
65 | let splitViewController = UISplitViewController()
66 | splitViewController.delegate = viewDelegate
67 | splitViewController.viewControllers = [tabBarController, viewDelegate.detailNavigationController]
68 | splitViewController.preferredDisplayMode = .allVisible
69 |
70 | window.rootViewController = splitViewController
71 | window.makeKeyAndVisible()
72 |
73 | return coordinationResults
74 | .take(1)
75 | }
76 |
77 | private func configure(tabBarController: UITabBarController, withTabs tabs: [SectionTab]) -> [Observable] {
78 | let navControllers = tabs
79 | .map { tab -> UINavigationController in
80 | let navController = NavigationController(withPopDetailCompletion: viewDelegate.replaceDetail)
81 | navController.tabBarItem = UITabBarItem(title: tab.title, image: tab.image, selectedImage: nil)
82 | //navController.navigationBar.prefersLargeTitles = true
83 | //navController.navigationItem.largeTitleDisplayMode = .automatic
84 | return navController
85 | }
86 |
87 | tabBarController.viewControllers = navControllers
88 | tabBarController.delegate = viewDelegate
89 | tabBarController.view.backgroundColor = UIColor.white // Fix dark shadow in nav bar on segue
90 |
91 | return zip(tabs, navControllers)
92 | .map { (tab, navCtrl) in
93 | switch tab {
94 | case .posts:
95 | let coordinator = PostsCoordinator(navigationController: navCtrl, dependencies: dependencies)
96 | return coordinate(to: coordinator)
97 | case .albums:
98 | let coordinator = AlbumsCoordinator(navigationController: navCtrl, dependencies: dependencies)
99 | return coordinate(to: coordinator)
100 | case .todos:
101 | let coordinator = TodosCoordinator(navigationController: navCtrl, dependencies: dependencies)
102 | return coordinate(to: coordinator)
103 | case .profile:
104 | let coordinator = ProfileCoordinator(navigationController: navCtrl, dependencies: dependencies)
105 | return coordinate(to: coordinator)
106 | }
107 | }
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/SplitViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SplitViewDelegate.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/8/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class SplitViewDelegate: NSObject {
12 |
13 | let detailNavigationController: UINavigationController
14 |
15 | init(detailNavigationController: UINavigationController) {
16 | self.detailNavigationController = detailNavigationController
17 | super.init()
18 | }
19 |
20 | /// Changes the view controller displayed in the detail navigation controller.
21 | ///
22 | /// - Parameters:
23 | /// - primaryContainer: The `PrimaryContainerType` containing the `.detailView` used to update the detail nav controller.
24 | /// - animated: If `true`, animates the update.
25 | func updateSecondaryWithDetail(from primaryContainer: PrimaryContainerType, animated: Bool = false) {
26 | switch primaryContainer.detailView {
27 | case .collapsed(let detailViewController):
28 | detailNavigationController.setViewControllers([detailViewController], animated: animated)
29 | case .separated(let detailViewController):
30 | detailNavigationController.setViewControllers([detailViewController], animated: animated)
31 | case .placeholder:
32 | detailNavigationController.setViewControllers([primaryContainer.makePlaceholderViewController()],
33 | animated: animated)
34 | }
35 | }
36 |
37 | /// Sets view of detail navigation controller to a placeholder view controller.
38 | ///
39 | /// - Parameter viewController: Placeholder view controller to use.
40 | func replaceDetail(withEmpty viewController: UIViewController & PlaceholderViewControllerType) {
41 | detailNavigationController.setViewControllers([viewController], animated: true)
42 | }
43 |
44 | }
45 |
46 | // MARK: - UITabBarControllerDelegate
47 | extension SplitViewDelegate: UITabBarControllerDelegate {
48 |
49 | func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
50 | /// Prevent selection of the same tab twice (which would reset its navigation controller)
51 | return tabBarController.selectedViewController === viewController ? false : true
52 | }
53 |
54 | func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
55 | guard
56 | let splitViewController = tabBarController.splitViewController,
57 | let selectedNavController = viewController as? PrimaryContainerType else {
58 | fatalError("\(#function) FAILED : wrong view controller type")
59 | }
60 | /// If split view controller is collapsed, detail view will already be on `selectedNavController.viewControllers`;
61 | /// otherwise, we need to change the secondary view controller to the selected tab's detail view.
62 | if !splitViewController.isCollapsed {
63 | updateSecondaryWithDetail(from: selectedNavController)
64 | }
65 | }
66 |
67 | }
68 |
69 | // MARK: - UISplitViewControllerDelegate
70 | extension SplitViewDelegate: UISplitViewControllerDelegate {
71 |
72 | // MARK: Collapsing the Interface
73 |
74 | func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
75 | guard
76 | let tabBarController = splitViewController.viewControllers.first as? UITabBarController,
77 | let navigationControllers = tabBarController.viewControllers as? [PrimaryContainerType] else {
78 | fatalError("\(#function) FAILED : wrong view controller type")
79 | }
80 |
81 | navigationControllers.forEach { $0.collapseDetail() }
82 | return true /// Prevent UIKit from performing default collapse behavior
83 | }
84 |
85 | // MARK: Expanding the Interface
86 |
87 | func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
88 | guard
89 | let tabBarController = primaryViewController as? UITabBarController,
90 | let navigationControllers = tabBarController.viewControllers as? [PrimaryContainerType],
91 | let selectedNavController = tabBarController.selectedViewController as? PrimaryContainerType else {
92 | fatalError("\(#function) FAILED : wrong view controller type")
93 | }
94 |
95 | navigationControllers.forEach { $0.separateDetail() }
96 |
97 | /// There is no point in hiding the primary view controller with a placeholder detail view
98 | if case .placeholder = selectedNavController.detailView, splitViewController.preferredDisplayMode == .primaryHidden {
99 | splitViewController.preferredDisplayMode = .allVisible
100 | }
101 | updateSecondaryWithDetail(from: selectedNavController)
102 | return detailNavigationController
103 | }
104 |
105 | // MARK: Overriding the Presentation Behavior
106 |
107 | func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
108 | guard
109 | let tabBarController = splitViewController.viewControllers.first as? UITabBarController,
110 | let selectedNavController = tabBarController.selectedViewController as? UINavigationController
111 | & PrimaryContainerType else {
112 | fatalError("\(#function) FAILED : wrong view controller type")
113 | }
114 |
115 | vc.navigationItem.leftItemsSupplementBackButton = true
116 | vc.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
117 |
118 | if splitViewController.isCollapsed {
119 | selectedNavController.pushViewController(vc, animated: true)
120 | selectedNavController.detailView = .collapsed(vc)
121 | } else {
122 | switch selectedNavController.detailView {
123 | /// Animate only the initial presentation of the detail vc
124 | case .placeholder:
125 | detailNavigationController.setViewControllers([vc], animated: true)
126 | default:
127 | detailNavigationController.setViewControllers([vc], animated: false)
128 | }
129 | selectedNavController.detailView = .separated(vc)
130 | }
131 | return true /// Prevent UIKit from performing default behavior
132 | }
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/TabBarController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/8/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TabBarController: UITabBarController {
12 |
13 | let detailNavigationController: UINavigationController
14 |
15 | // MARK: - Lifecycle
16 |
17 | init(detailNavigationController: UINavigationController) {
18 | self.detailNavigationController = detailNavigationController
19 | super.init(nibName: nil, bundle: nil)
20 | delegate = self
21 | }
22 |
23 | required init?(coder aDecoder: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | // MARK: -
28 |
29 | func updateSecondaryWithDetail(from primaryContainer: PrimaryContainerType, animated: Bool = false) {
30 | switch primaryContainer.detailView {
31 | case .collapsed(let detailViewController):
32 | detailNavigationController.setViewControllers([detailViewController], animated: animated)
33 | case .separated(let detailViewController):
34 | detailNavigationController.setViewControllers([detailViewController], animated: animated)
35 | case .placeholder:
36 | detailNavigationController.setViewControllers([primaryContainer.makePlaceholderViewController()],
37 | animated: animated)
38 | }
39 | }
40 |
41 | func replaceDetail(withEmpty viewController: UIViewController & PlaceholderViewControllerType) {
42 | detailNavigationController.setViewControllers([viewController], animated: true)
43 | }
44 |
45 | }
46 |
47 | // MARK: - UITabBarControllerDelegate
48 | extension TabBarController: UITabBarControllerDelegate {
49 |
50 | func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
51 | // Prevent selection of the same tab twice (which would reset its navigation controller)
52 | return selectedViewController === viewController ? false : true
53 | }
54 |
55 | func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
56 | guard let svc = splitViewController, let selectedNavController = viewController as? PrimaryContainerType else {
57 | fatalError("Wrong view controller type: \(viewController)")
58 | }
59 | // If split view controller is collapsed, detail view will already be on `selectedNavController.viewControllers`;
60 | // otherwise, we need to change the secondary view controller to the selected tab's detail view.
61 | if !svc.isCollapsed {
62 | updateSecondaryWithDetail(from: selectedNavController)
63 | }
64 | }
65 |
66 | }
67 |
68 | // MARK: - UISplitViewControllerDelegate
69 | extension TabBarController: UISplitViewControllerDelegate {
70 |
71 | // MARK: Collapsing the Interface
72 |
73 | func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
74 | guard let navigationControllers = viewControllers as? [PrimaryContainerType] else {
75 | fatalError("Wrong view controller type: \(String(describing: viewControllers?.filter { !($0 is PrimaryContainerType) }))")
76 | }
77 |
78 | navigationControllers.forEach { $0.collapseDetail() }
79 | return true // Prevent UIKit from performing default collapse behavior
80 | }
81 |
82 | // MARK: Expanding the Interface
83 |
84 | func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
85 | guard
86 | let navigationControllers = viewControllers as? [PrimaryContainerType],
87 | let selectedNavController = selectedViewController as? PrimaryContainerType else {
88 | fatalError("Wrong view controller type: \(String(describing: viewControllers?.filter { !($0 is PrimaryContainerType) }))")
89 | }
90 |
91 | navigationControllers.forEach { $0.separateDetail() }
92 |
93 | // There is no point in hiding the primary view controller with a placeholder detail view
94 | if case .placeholder = selectedNavController.detailView, splitViewController.preferredDisplayMode == .primaryHidden {
95 | splitViewController.preferredDisplayMode = .allVisible
96 | }
97 | updateSecondaryWithDetail(from: selectedNavController)
98 | return detailNavigationController
99 | }
100 |
101 | // MARK: Overriding the Presentation Behavior
102 |
103 | func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
104 | guard let selectedNavController = selectedViewController as? UINavigationController & PrimaryContainerType else {
105 | fatalError("\(#function) FAILED : wrong view controller type")
106 | }
107 |
108 | vc.navigationItem.leftItemsSupplementBackButton = true
109 | vc.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
110 |
111 | if splitViewController.isCollapsed {
112 | selectedNavController.pushViewController(vc, animated: true)
113 | selectedNavController.detailView = .collapsed(vc)
114 | } else {
115 | switch selectedNavController.detailView {
116 | // Animate only the initial presentation of the detail vc
117 | case .placeholder:
118 | detailNavigationController.setViewControllers([vc], animated: true)
119 | default:
120 | detailNavigationController.setViewControllers([vc], animated: false)
121 | }
122 | selectedNavController.detailView = .separated(vc)
123 | }
124 | return true // Prevent UIKit from performing default behavior
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/TabBarCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SplitViewCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | class SplitViewCoordinator: BaseCoordinator {
12 | typealias Dependencies = HasClient & HasUserManager
13 |
14 | private let window: UIWindow
15 | private let dependencies: Dependencies
16 |
17 | private let splitViewController: UISplitViewController
18 | private let tabBarController: TabBarController
19 | // swiftlint:disable:next weak_delegate
20 | private var viewDelegate: SplitViewDelegate?
21 |
22 | enum SectionTab {
23 | case posts
24 | case todos
25 | case settings
26 |
27 | var title: String {
28 | switch self {
29 | case .posts: return "Posts"
30 | case .todos: return "Todos"
31 | case .settings: return "Settings"
32 | }
33 | }
34 |
35 | var image: UIImage {
36 | switch self {
37 | case .posts: return #imageLiteral(resourceName: "PostsTabIcon")
38 | case .todos: return #imageLiteral(resourceName: "TodosTabIcon")
39 | case .settings: return #imageLiteral(resourceName: "Settings")
40 | }
41 | }
42 |
43 | var tag: Int {
44 | switch self {
45 | case .posts: return 0
46 | case .todos: return 1
47 | case .settings: return 2
48 | }
49 | }
50 | }
51 |
52 | // MARK: - Lifecycle
53 |
54 | init(window: UIWindow, dependencies: Dependencies) {
55 | self.window = window
56 | self.dependencies = dependencies
57 | self.splitViewController = UISplitViewController()
58 | self.tabBarController = TabBarController()
59 | }
60 |
61 | override func start() -> Observable {
62 | let tabBarController = TabBarController()
63 | let tabs: [SectionTab] = [.posts, .todos, .settings]
64 | let coordinationResults = Observable.from(configure(tabBarController: tabBarController, withTabs: tabs)).merge()
65 |
66 | self.viewDelegate = SplitViewDelegate(splitViewController: splitViewController,
67 | tabBarController: tabBarController)
68 |
69 | window.rootViewController = splitViewController
70 | window.makeKeyAndVisible()
71 |
72 | return coordinationResults
73 | }
74 |
75 | private func configure(tabBarController: UITabBarController, withTabs tabs: [SectionTab]) -> [Observable] {
76 | let navControllers = tabs
77 | .map { tab -> UINavigationController in
78 | let navController = NavigationController()
79 | navController.tabBarItem = UITabBarItem(title: tab.title, image: tab.image, tag: tab.tag)
80 | //navController.navigationBar.prefersLargeTitles = true
81 | //navController.navigationItem.largeTitleDisplayMode = .automatic
82 | return navController
83 | }
84 |
85 | tabBarController.viewControllers = navControllers
86 | tabBarController.view.backgroundColor = UIColor.white // Fix dark shadow in nav bar on segue
87 |
88 | return zip(tabs, navControllers)
89 | .map { (tab, navCtrl) in
90 | switch tab {
91 | case .posts:
92 | let coordinator = PostsCoordinator(navigationController: navCtrl, dependencies: dependencies)
93 | return coordinate(to: coordinator)
94 | case .todos:
95 | let coordinator = TodosCoordinator(navigationController: navCtrl, dependencies: dependencies)
96 | return coordinate(to: coordinator)
97 | case .settings:
98 | let coordinator = SettingsCoordinator(navigationController: navCtrl, dependencies: dependencies)
99 | return coordinate(to: coordinator)
100 | }
101 | }
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Common/TableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/8/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | class TableViewController: UITableViewController, ErrorAlertDisplayable {
13 |
14 | let disposeBag = DisposeBag()
15 |
16 | // MARK: - Lifecycle
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | setupTableView()
21 | }
22 |
23 | // MARK: - View Methods
24 |
25 | func setupTableView() {
26 | // This is necessary since UITableViewController automatically sets tableview delegate and dataSource to self
27 | tableView.delegate = nil
28 | tableView.dataSource = nil
29 |
30 | tableView.tableFooterView = UIView() // Prevent empty rows
31 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.reuseID)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Login/LoginCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/1/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class LoginCoordinator: BaseCoordinator {
14 | typealias Dependencies = HasUserManager
15 |
16 | private let window: UIWindow
17 | private let dependencies: Dependencies
18 |
19 | init(window: UIWindow, dependencies: Dependencies) {
20 | self.window = window
21 | self.dependencies = dependencies
22 | }
23 |
24 | override func start() -> Observable {
25 | let viewController = LoginViewController()
26 | let avm: Attachable = .detached(dependencies)
27 | let viewModel = viewController.attach(wrapper: avm)
28 |
29 | let login = viewModel.loggedIn
30 | .asObservable()
31 | .filter { $0 }
32 | .map { _ in return }
33 |
34 | let signup = viewModel.signupTaps
35 | .asObservable()
36 | .flatMap { [weak self] _ -> Observable in
37 | guard let strongSelf = self else { return .empty() }
38 | return strongSelf.showSignup(on: viewController)
39 | }
40 | .filter { $0 != SignupCoordinationResult.cancel }
41 | .map { _ in return }
42 |
43 | window.rootViewController = viewController
44 | window.makeKeyAndVisible()
45 |
46 | return Observable.merge(login, signup)
47 | .take(1)
48 | }
49 |
50 | private func showSignup(on rootViewController: UIViewController) -> Observable {
51 | let signupCoordinator = SignupCoordinator(rootViewController: rootViewController, dependencies: dependencies)
52 | return coordinate(to: signupCoordinator)
53 | }
54 |
55 | }
56 |
57 | // MARK: - Modal Presentation
58 |
59 | enum ModalLoginCoordinationResult {
60 | case login
61 | case cancel
62 | }
63 |
64 | class ModalLoginCoordinator: BaseCoordinator {
65 | typealias Dependencies = HasUserManager
66 |
67 | private let rootViewController: UIViewController
68 | private let dependencies: Dependencies
69 |
70 | init(rootViewController: UIViewController, dependencies: Dependencies) {
71 | self.rootViewController = rootViewController
72 | self.dependencies = dependencies
73 | }
74 |
75 | override func start() -> Observable {
76 | let viewController = LoginViewController()
77 | let navigationController = UINavigationController(rootViewController: viewController)
78 |
79 | let avm: Attachable = .detached(dependencies)
80 | let viewModel = viewController.attach(wrapper: avm)
81 |
82 | let login = viewModel.loggedIn
83 | .filter { $0 }
84 | .map { _ in return ModalLoginCoordinationResult.login }
85 |
86 | let cancel = viewModel.cancelTaps
87 | .map { _ in return ModalLoginCoordinationResult.cancel }
88 |
89 | rootViewController.present(navigationController, animated: true)
90 |
91 | return Driver.merge(cancel, login)
92 | .asObservable()
93 | .take(1)
94 | .do(onNext: { [weak self] _ in self?.rootViewController.dismiss(animated: true) })
95 | }
96 |
97 | private func showSignup(on rootViewController: UIViewController) -> Observable {
98 | let signupCoordinator = SignupCoordinator(rootViewController: rootViewController, dependencies: dependencies)
99 | return coordinate(to: signupCoordinator)
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Login/LoginViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/1/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 | import ColorCompatibility
13 |
14 | class LoginViewController: UIViewController, ViewModelAttaching {
15 |
16 | var viewModel: Attachable!
17 | lazy var bindings: LoginViewModel.Bindings = {
18 | return LoginViewModel.Bindings(
19 | username: loginTextField.rx.text.orEmpty.asDriver(),
20 | password: passwordTextField.rx.text.orEmpty.asDriver(),
21 | loginTaps: loginButton.rx.tap.asDriver(),
22 | signupTaps: signupButton.rx.tap.asDriver(),
23 | doneTaps: passwordTextField.rx.controlEvent(.editingDidEndOnExit).asDriver(),
24 | cancelTaps: cancelButtonItem.rx.tap.asDriver()
25 | )
26 | }()
27 |
28 | let disposeBag = DisposeBag()
29 |
30 | // MARK: Interface
31 |
32 | let loginTextField: UITextField = {
33 | let view = UITextField()
34 | view.placeholder = "Email"
35 | view.translatesAutoresizingMaskIntoConstraints = false
36 | view.borderStyle = .roundedRect
37 | view.clearButtonMode = .whileEditing
38 | view.textAlignment = .left
39 | // UITextInputTraits
40 | view.autocapitalizationType = .none
41 | view.autocorrectionType = .no
42 | view.keyboardType = .emailAddress
43 | view.returnKeyType = .next
44 | view.spellCheckingType = .no
45 | view.textContentType = .emailAddress
46 | return view
47 | }()
48 |
49 | let passwordTextField: UITextField = {
50 | let view = UITextField()
51 | view.placeholder = "Password"
52 | view.isSecureTextEntry = true
53 | view.translatesAutoresizingMaskIntoConstraints = false
54 | view.borderStyle = .roundedRect
55 | view.clearButtonMode = .whileEditing
56 | view.textAlignment = .left
57 | // UITextInputTraits
58 | view.autocapitalizationType = .none
59 | view.returnKeyType = .go
60 | view.spellCheckingType = .no
61 | return view
62 | }()
63 |
64 | let loginButton: UIButton = {
65 | let view = UIButton()
66 | view.setTitle("Login", for: .normal)
67 | view.translatesAutoresizingMaskIntoConstraints = false
68 | view.layer.cornerRadius = 5
69 | view.clipsToBounds = true
70 | view.setTitleColor(ColorCompatibility.systemRed, for: .highlighted)
71 | view.backgroundColor = ColorCompatibility.systemBlue
72 | return view
73 | }()
74 |
75 | let signupButton: UIButton = {
76 | let view = UIButton()
77 | view.setTitle("Signup", for: .normal)
78 | view.translatesAutoresizingMaskIntoConstraints = false
79 | view.layer.cornerRadius = 5
80 | view.layer.borderWidth = 1
81 | view.layer.borderColor = ColorCompatibility.systemBlue.cgColor
82 | view.setTitleColor(ColorCompatibility.systemBlue, for: .normal)
83 | view.setTitleColor(ColorCompatibility.systemRed, for: .highlighted)
84 | view.backgroundColor = .clear
85 | return view
86 | }()
87 |
88 | private lazy var stackView: UIStackView = {
89 | let view = UIStackView(arrangedSubviews: [loginTextField, passwordTextField, loginButton, signupButton])
90 | view.axis = .vertical
91 | view.alignment = .fill
92 | view.distribution = .fillEqually
93 | view.spacing = 8.0
94 | view.translatesAutoresizingMaskIntoConstraints = false
95 | return view
96 | }()
97 |
98 | let cancelButtonItem = UIBarButtonItem(
99 | barButtonSystemItem: .cancel,
100 | target: LoginViewController.self,
101 | action: nil)
102 |
103 | // MARK: - Lifecycle
104 |
105 | override func viewDidLoad() {
106 | super.viewDidLoad()
107 | setupView()
108 | setupConstraints()
109 | }
110 |
111 | override func viewWillAppear(_ animated: Bool) {
112 | if self.presentingViewController != nil {
113 | navigationItem.leftBarButtonItem = cancelButtonItem
114 | signupButton.isHidden = true
115 | }
116 | }
117 |
118 | // MARK: - View Methods
119 |
120 | private func setupView() {
121 | view.backgroundColor = ColorCompatibility.systemBackground
122 | view.addSubview(stackView)
123 | }
124 |
125 | private func setupConstraints() {
126 | let guide = view.safeAreaLayoutGuide
127 | let height = view.frame.height
128 | NSLayoutConstraint.activate([
129 | stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
130 | stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: height / 4.0),
131 | stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 16.0),
132 | stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -16.0)
133 | //stackView.widthAnchor.constraint(lessThanOrEqualToConstant: 380.0)
134 | ])
135 | }
136 |
137 | func bind(viewModel: LoginViewModel) -> LoginViewModel {
138 | viewModel.isValid
139 | .drive(loginButton.rx.isEnabled)
140 | .disposed(by: disposeBag)
141 |
142 | viewModel.loggingIn
143 | .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
144 | .disposed(by: disposeBag)
145 |
146 | // Next keyboard button
147 | loginTextField.rx.controlEvent(.editingDidEndOnExit)
148 | .subscribe(onNext: { [weak self] _ in
149 | self?.passwordTextField.becomeFirstResponder()
150 | })
151 | .disposed(by: disposeBag)
152 |
153 | return viewModel
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Login/LoginViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/1/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | final class LoginViewModel: ViewModelType {
13 |
14 | let isValid: Driver
15 | let loggingIn: Driver
16 | let loggedIn: Driver
17 | let signupTaps: Driver
18 | let cancelTaps: Driver
19 |
20 | // MARK: - Lifecycle
21 |
22 | init(dependency: Dependency, bindings: Bindings) {
23 | let userInputs = Driver.combineLatest(
24 | bindings.username, bindings.password
25 | ) { (username, password) -> (String, String) in
26 | return (username, password)
27 | }
28 |
29 | isValid = userInputs
30 | .map { username, password in
31 | return username.count > 0 && password.count > 0
32 | }
33 |
34 | let loggingIn = ActivityIndicator()
35 | self.loggingIn = loggingIn.asDriver()
36 |
37 | loggedIn = Driver.merge(bindings.loginTaps, bindings.doneTaps)
38 | .withLatestFrom(userInputs)
39 | .flatMap { (arg) -> Driver in
40 | let (username, password) = arg
41 | return dependency.userManager.login(username: username, password: password)
42 | .trackActivity(loggingIn)
43 | .asDriver(onErrorJustReturn: false)
44 | }
45 | //.share(replay: 1)
46 |
47 | signupTaps = bindings.signupTaps
48 | cancelTaps = bindings.cancelTaps
49 | }
50 |
51 | // MARK: - ViewModelType
52 |
53 | typealias Dependency = HasUserManager
54 |
55 | struct Bindings {
56 | let username: Driver
57 | let password: Driver
58 | let loginTaps: Driver
59 | let signupTaps: Driver
60 | let doneTaps: Driver
61 | let cancelTaps: Driver
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostDetail/PostDetailViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostDetail/PostDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostDetailViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | final class PostDetailViewController: UIViewController {
14 |
15 | @IBOutlet weak var titleLabel: UILabel!
16 | @IBOutlet weak var bodyLabel: UILabel!
17 |
18 | let disposeBag = DisposeBag()
19 | var viewModel: PostDetailViewModel!
20 |
21 | // MARK: - Lifecycle
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 | setupView()
26 | }
27 |
28 | // MARK: - View Methods
29 |
30 | private func setupView() {
31 | bindViewModel()
32 | }
33 |
34 | func bindViewModel() {
35 | viewModel.title
36 | .drive(titleLabel.rx.text)
37 | .disposed(by: disposeBag)
38 |
39 | viewModel.body
40 | .drive(bodyLabel.rx.text)
41 | .disposed(by: disposeBag)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostDetail/PostDetailViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostDetailViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | final class PostDetailViewModel {
13 | let title: Driver
14 | let body: Driver
15 | let post: Post
16 |
17 | // MARK: - Lifecycle
18 |
19 | init(post: Post) {
20 | self.post = post
21 | self.title = Observable.just(post.title).asDriver(onErrorJustReturn: "Error")
22 | self.body = Observable.just(post.body).asDriver(onErrorJustReturn: "Error")
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostsCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostsCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | class PostsCoordinator: BaseCoordinator {
12 | typealias Dependencies = HasPostService
13 |
14 | private let navigationController: UINavigationController
15 | private let dependencies: Dependencies
16 |
17 | init(navigationController: UINavigationController, dependencies: Dependencies) {
18 | self.navigationController = navigationController
19 | self.dependencies = dependencies
20 | }
21 |
22 | override func start() -> Observable {
23 | let viewController = PostsListViewController.instance()
24 | navigationController.viewControllers = [viewController]
25 |
26 | let avm: Attachable = .detached(dependencies)
27 | let viewModel = viewController.attach(wrapper: avm)
28 |
29 | viewModel.selectedPost
30 | .drive(onNext: { [weak self] selection in
31 | self?.showDetailView(with: selection)
32 | })
33 | .disposed(by: viewController.disposeBag)
34 |
35 | // View will never be dismissed
36 | return Observable.never()
37 | }
38 |
39 | private func showDetailView(with post: Post) {
40 | let viewController = PostDetailViewController.instance()
41 | viewController.viewModel = PostDetailViewModel(post: post)
42 | navigationController.showDetailViewController(viewController, sender: nil)
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostsList/PostTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostTableViewCell.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/16/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | final class PostTableViewCell: UITableViewCell {
14 | @IBOutlet weak var titleLabel: UILabel!
15 | @IBOutlet weak var bodyLabel: UILabel!
16 |
17 | private(set) var disposeBag = DisposeBag()
18 |
19 | override func prepareForReuse() {
20 | super.prepareForReuse()
21 | disposeBag = DisposeBag()
22 | }
23 |
24 | func bind(to viewModel: PostDetailViewModel) {
25 | viewModel.title
26 | .drive(titleLabel.rx.text)
27 | .disposed(by: disposeBag)
28 |
29 | viewModel.body
30 | .drive(bodyLabel.rx.text)
31 | .disposed(by: disposeBag)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostsList/PostsListViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostsList/PostsListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostsListViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | final class PostsListViewController: TableViewController, ViewModelAttaching {
14 |
15 | var viewModel: Attachable!
16 | var bindings: PostsListViewModel.Bindings {
17 | let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
18 | .mapToVoid()
19 | .asDriverOnErrorJustComplete()
20 | let refresh = tableView.refreshControl!.rx
21 | .controlEvent(.valueChanged)
22 | .asDriver()
23 |
24 | return PostsListViewModel.Bindings(
25 | fetchTrigger: Driver.merge(viewWillAppear, refresh),
26 | selection: tableView.rx.itemSelected.asDriver()
27 | )
28 | }
29 |
30 | // MARK: - Lifecycle
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | setupView()
35 | }
36 |
37 | // MARK: - View Methods
38 |
39 | private func setupView() {
40 | title = "Posts"
41 | }
42 |
43 | func bind(viewModel: PostsListViewModel) -> PostsListViewModel {
44 | viewModel.posts
45 | .drive(tableView.rx.items(cellIdentifier: PostTableViewCell.reuseID, cellType: PostTableViewCell.self)) { _, viewModel, cell in
46 | cell.bind(to: viewModel)
47 | }
48 | .disposed(by: disposeBag)
49 |
50 | viewModel.fetching
51 | .drive(tableView.refreshControl!.rx.isRefreshing)
52 | .disposed(by: disposeBag)
53 |
54 | viewModel.errors
55 | .delay(.milliseconds(100))
56 | .map { $0.localizedDescription }
57 | .drive(errorAlert)
58 | .disposed(by: disposeBag)
59 |
60 | return viewModel
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Posts/PostsList/PostsListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostsListViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | final class PostsListViewModel: ViewModelType {
13 |
14 | let fetching: Driver
15 | let posts: Driver<[PostDetailViewModel]>
16 | let selectedPost: Driver
17 | let errors: Driver
18 |
19 | // MARK: - Lifecycle
20 |
21 | init(dependency: Dependency, bindings: Bindings) {
22 | let activityIndicator = ActivityIndicator()
23 | let errorTracker = ErrorTracker()
24 |
25 | posts = bindings.fetchTrigger
26 | .flatMapLatest {
27 | return dependency.postService.getPosts()
28 | .trackActivity(activityIndicator)
29 | .trackError(errorTracker)
30 | .asDriverOnErrorJustComplete()
31 | .map { $0.map(PostDetailViewModel.init) }
32 | }
33 |
34 | fetching = activityIndicator.asDriver()
35 | errors = errorTracker.asDriver()
36 | selectedPost = bindings.selection
37 | .withLatestFrom(self.posts) { (indexPath, posts: [PostDetailViewModel]) -> Post in
38 | return posts[indexPath.row].post
39 | }
40 | }
41 |
42 | // MARK: - ViewModelType
43 |
44 | typealias Dependency = HasPostService
45 |
46 | struct Bindings {
47 | let fetchTrigger: Driver
48 | let selection: Driver
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Profile/ProfileCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/12/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class ProfileCoordinator: BaseCoordinator {
14 | typealias Dependencies = HasUserManager
15 |
16 | private let navigationController: UINavigationController
17 | private let dependencies: Dependencies
18 |
19 | init(navigationController: UINavigationController, dependencies: Dependencies) {
20 | self.navigationController = navigationController
21 | self.dependencies = dependencies
22 | }
23 |
24 | override func start() -> Observable {
25 | let viewController = ProfileViewController.instance()
26 | navigationController.viewControllers = [viewController]
27 |
28 | let avm: Attachable = .detached(dependencies)
29 | let viewModel = viewController.attach(wrapper: avm)
30 |
31 | return viewModel.settingsTap
32 | .asObservable()
33 | .flatMap { [weak self] _ -> Observable in
34 | guard let strongSelf = self else { return .empty() }
35 | return strongSelf.showSettings(on: viewController)
36 | }
37 | .filter { $0 != .none }
38 | .map { _ in return }
39 | }
40 |
41 | private func showSettings(on rootViewController: UIViewController) -> Observable {
42 | let settingsCoordinator = SettingsCoordinator(rootViewController: rootViewController,
43 | dependencies: dependencies)
44 | return coordinate(to: settingsCoordinator)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Profile/ProfileViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Profile/ProfileViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/12/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class ProfileViewController: UIViewController, ViewModelAttaching {
14 |
15 | var viewModel: Attachable!
16 | var bindings: ProfileViewModel.Bindings {
17 | return ProfileViewModel.Bindings(
18 | settingsTaps: settingsButtonItem.rx.tap.asDriver()
19 | )
20 | }
21 |
22 | let disposeBag = DisposeBag()
23 |
24 | // MARK: Interface
25 |
26 | @IBOutlet weak var settingsButtonItem: UIBarButtonItem!
27 | @IBOutlet weak var avatarView: UIView!
28 | @IBOutlet weak var avatarLabel: UILabel!
29 | @IBOutlet weak var nameLabel: UILabel!
30 | @IBOutlet weak var usernameLabel: UILabel!
31 |
32 | // MARK: Lifecycle
33 |
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 | setupView()
37 | }
38 |
39 | // MARK: - View Methods
40 |
41 | private func setupView() {
42 | avatarView.layer.cornerRadius = 35
43 | avatarView.clipsToBounds = true
44 | }
45 |
46 | func bind(viewModel: ProfileViewModel) -> ProfileViewModel {
47 | viewModel.initials.drive(avatarLabel.rx.text)
48 | .disposed(by: disposeBag)
49 |
50 | viewModel.name.drive(nameLabel.rx.text)
51 | .disposed(by: disposeBag)
52 |
53 | viewModel.username.drive(usernameLabel.rx.text)
54 | .disposed(by: disposeBag)
55 |
56 | return viewModel
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Profile/ProfileViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/12/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 | import RxSwiftExt
12 |
13 | final class ProfileViewModel: ViewModelType {
14 |
15 | let initials: Driver
16 | let name: Driver
17 | let username: Driver
18 | let settingsTap: Driver
19 |
20 | // MARK: - Lifecycle
21 |
22 | init(dependency: Dependency, bindings: Bindings) {
23 | let currentUser = dependency.userManager.currentUser
24 | .unwrap()
25 |
26 | name = currentUser
27 | .map { $0.name }
28 | .asDriver(onErrorJustReturn: "Error")
29 |
30 | initials = name
31 | // https://stackoverflow.com/questions/35285978/get-the-initials-from-a-name-and-limit-it-to-2-initials
32 | .map { $0.components(separatedBy: " ").reduce("") { ($0 == "" ? "" : "\($0.first ?? "X")") + "\($1.first ?? "X")" } }
33 | .asDriver(onErrorJustReturn: "Error")
34 |
35 | username = currentUser
36 | .map { $0.username }
37 | .asDriver(onErrorJustReturn: "Error")
38 |
39 | settingsTap = bindings.settingsTaps
40 | }
41 |
42 | // MARK: - ViewModelType
43 |
44 | typealias Dependency = HasUserManager
45 |
46 | struct Bindings {
47 | let settingsTaps: Driver
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Settings/SettingsCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/4/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | /// Type defining possible coordination results of the `SettingsCoordinator`.
12 | ///
13 | /// - none: No changes to user authentication status.
14 | /// - logout: User logged out.
15 | /// - login: User logged out and back in again (possibly as a different user).
16 | enum SettingsCoordinationResult {
17 | case none
18 | case logout
19 | case login
20 | }
21 |
22 | class SettingsCoordinator: BaseCoordinator {
23 | typealias Dependencies = HasUserManager
24 |
25 | private let rootViewController: UIViewController
26 | private let dependencies: Dependencies
27 |
28 | init(rootViewController: UIViewController, dependencies: Dependencies) {
29 | self.rootViewController = rootViewController
30 | self.dependencies = dependencies
31 | }
32 |
33 | override func start() -> Observable {
34 | let viewController = SettingsViewController.instance()
35 | let navigationController = UINavigationController(rootViewController: viewController)
36 |
37 | let avm: Attachable = .detached(dependencies)
38 | let viewModel = viewController.attach(wrapper: avm)
39 |
40 | let login = viewModel.showLogin
41 | .asObservable()
42 | .flatMap { [weak self] _ -> Observable in
43 | guard let strongSelf = self else { return .empty() }
44 | return strongSelf.showLogin(on: viewController)
45 | }
46 | .filter { $0 == .login }
47 | .map { _ in return SettingsCoordinationResult.login }
48 |
49 | let logout = viewModel.didLogout
50 | .asObservable()
51 | .map { _ in return SettingsCoordinationResult.logout }
52 |
53 | let authenticationChanges = Observable.of(logout, login)
54 | .merge()
55 | .startWith(SettingsCoordinationResult.none)
56 |
57 | if let navVC = rootViewController.parent as? UINavigationController, let tabVC = navVC.parent,
58 | let splitVC = tabVC.parent, splitVC.traitCollection.horizontalSizeClass == .regular {
59 | navigationController.modalPresentationStyle = .formSheet
60 | }
61 |
62 | rootViewController.present(navigationController, animated: true)
63 |
64 | return viewController.doneButtonItem.rx.tap
65 | .take(1)
66 | .withLatestFrom(authenticationChanges)
67 | .do(onNext: { [weak self] _ in self?.rootViewController.dismiss(animated: true) })
68 | }
69 |
70 | private func showLogin(on rootViewController: UIViewController) -> Observable {
71 | let loginCoordinator = ModalLoginCoordinator(rootViewController: rootViewController, dependencies: dependencies)
72 | return coordinate(to: loginCoordinator)
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Settings/SettingsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/4/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class SettingsViewController: UITableViewController, ViewModelAttaching {
14 |
15 | lazy var bindings: SettingsViewModel.Bindings = {
16 | return SettingsViewModel.Bindings(
17 | selection: tableView.rx.itemSelected.asDriver()
18 | )
19 | }()
20 |
21 | let disposeBag = DisposeBag()
22 | var viewModel: Attachable!
23 |
24 | // MARK: - Interface
25 |
26 | @IBOutlet weak var doneButtonItem: UIBarButtonItem!
27 | @IBOutlet weak var accountCell: UITableViewCell!
28 |
29 | // MARK: - Lifecycle
30 |
31 | override func viewDidLoad() {
32 | super.viewDidLoad()
33 | setupView()
34 | }
35 |
36 | // MARK: - View Methods
37 |
38 | private func setupView() {
39 | title = "Settings"
40 | }
41 |
42 | func bind(viewModel: SettingsViewModel) -> SettingsViewModel {
43 | viewModel.didLogout
44 | .drive()
45 | .disposed(by: disposeBag)
46 |
47 | viewModel.accountCellText
48 | .drive(accountCell.textLabel!.rx.text)
49 | .disposed(by: disposeBag)
50 |
51 | return viewModel
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Settings/SettingsViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/4/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | struct SettingsViewModel: ViewModelType {
13 |
14 | let accountCellText: Driver
15 | let didLogout: Driver
16 | let showLogin: Driver
17 |
18 | // MARK: - Lifecycle
19 |
20 | init(dependency: Dependency, bindings: Bindings) {
21 |
22 | accountCellText = dependency.userManager.currentUser
23 | .map { user in
24 | return user != nil ? "Logout \(user!.username)" : "Login"
25 | }
26 | .asDriver(onErrorJustReturn: "Error")
27 |
28 | let accountCellTaps = bindings.selection
29 | .filter { $0.section == 0 }
30 |
31 | didLogout = accountCellTaps
32 | .filter { _ in dependency.userManager.authenticationState == .signedIn }
33 | .flatMap { _ in
34 | return dependency.userManager.logout()
35 | .asDriver(onErrorJustReturn: false)
36 | }
37 |
38 | showLogin = accountCellTaps
39 | .filter { _ in dependency.userManager.authenticationState == .signedOut }
40 | .map { _ in return }
41 | }
42 |
43 | // MARK: - ViewModelType
44 |
45 | typealias Dependency = HasUserManager
46 |
47 | struct Bindings {
48 | let selection: Driver
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Signup/SignupCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignupCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/1/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | /// Type defining possible coordination results of the `SignupCoordinator`.
13 | ///
14 | /// - signUp: Signup completed successfully.
15 | /// - cancel: Cancel button was tapped.
16 | enum SignupCoordinationResult {
17 | case signUp
18 | case cancel
19 | }
20 |
21 | class SignupCoordinator: BaseCoordinator {
22 | typealias Dependencies = HasUserManager
23 |
24 | private let rootViewController: UIViewController
25 | private let dependencies: Dependencies
26 |
27 | init(rootViewController: UIViewController, dependencies: Dependencies) {
28 | self.rootViewController = rootViewController
29 | self.dependencies = dependencies
30 | }
31 |
32 | override func start() -> Observable {
33 | let viewController = SignupViewController.instance()
34 | let navigationController = UINavigationController(rootViewController: viewController)
35 |
36 | let avm: Attachable = .detached(dependencies)
37 | let viewModel = viewController.attach(wrapper: avm)
38 |
39 | let cancel = viewModel.cancelled
40 | .map { _ in CoordinationResult.cancel }
41 | let signUp = viewModel.signedUp
42 | .map { _ in CoordinationResult.signUp }
43 |
44 | rootViewController.present(navigationController, animated: true)
45 |
46 | return Driver.merge(cancel, signUp)
47 | .asObservable()
48 | .take(1)
49 | .do(onNext: { [weak self] _ in self?.rootViewController.dismiss(animated: true) })
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Signup/SignupViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Signup/SignupViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignupViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/1/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | class SignupViewController: UIViewController, ViewModelAttaching {
14 |
15 | var viewModel: Attachable!
16 | lazy var bindings: SignupViewModel.Bindings = {
17 | return SignupViewModel.Bindings(
18 | firstName: firstNameTextField.rx.text.orEmpty.asDriver(),
19 | lastName: lastNameTextField.rx.text.orEmpty.asDriver(),
20 | login: loginTextField.rx.text.orEmpty.asDriver(),
21 | password: passwordTextField.rx.text.orEmpty.asDriver(),
22 | cancelTaps: cancelButton.rx.tap.asDriver(),
23 | signupTaps: signupButton.rx.tap.asDriver(),
24 | doneTaps: passwordTextField.rx.controlEvent(.editingDidEndOnExit).asDriver()
25 | )
26 | }()
27 |
28 | let disposeBag = DisposeBag()
29 |
30 | // MARK: Interface
31 | let cancelButton = UIBarButtonItem(
32 | barButtonSystemItem: .cancel,
33 | target: SignupViewController.self,
34 | action: nil)
35 | @IBOutlet weak var firstNameTextField: UITextField!
36 | @IBOutlet weak var lastNameTextField: UITextField!
37 | @IBOutlet weak var loginTextField: UITextField!
38 | @IBOutlet weak var passwordTextField: UITextField!
39 | @IBOutlet weak var signupButton: UIButton!
40 |
41 | // MARK: Lifecycle
42 |
43 | override func viewDidLoad() {
44 | super.viewDidLoad()
45 | setupView()
46 | }
47 |
48 | // MARK: - View Methods
49 |
50 | private func setupView() {
51 | title = "Signup"
52 | self.navigationItem.leftBarButtonItem = cancelButton
53 | signupButton.layer.cornerRadius = 5
54 |
55 | // Next keyboard button
56 | firstNameTextField.rx.controlEvent(.editingDidEndOnExit)
57 | .subscribe(onNext: { [weak self] _ in
58 | self?.lastNameTextField.becomeFirstResponder()
59 | })
60 | .disposed(by: disposeBag)
61 |
62 | lastNameTextField.rx.controlEvent(.editingDidEndOnExit)
63 | .subscribe(onNext: { [weak self] _ in
64 | self?.loginTextField.becomeFirstResponder()
65 | })
66 | .disposed(by: disposeBag)
67 |
68 | loginTextField.rx.controlEvent(.editingDidEndOnExit)
69 | .subscribe(onNext: { [weak self] _ in
70 | self?.passwordTextField.becomeFirstResponder()
71 | })
72 | .disposed(by: disposeBag)
73 | }
74 |
75 | func bind(viewModel: SignupViewModel) -> SignupViewModel {
76 | viewModel.isValid
77 | .drive(signupButton.rx.isEnabled)
78 | .disposed(by: disposeBag)
79 |
80 | viewModel.signingUp
81 | .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
82 | .disposed(by: disposeBag)
83 |
84 | return viewModel
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Signup/SignupViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignupViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/1/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | final class SignupViewModel: ViewModelType {
13 |
14 | let isValid: Driver
15 | let signingUp: Driver
16 | let signedUp: Driver
17 | let cancelled: Driver
18 |
19 | // MARK: - Lifecycle
20 |
21 | init(dependency: Dependency, bindings: Bindings) {
22 | let userInputs = Driver.combineLatest(
23 | bindings.firstName, bindings.lastName, bindings.login, bindings.password
24 | ) { (firstName, lastName, username, password) -> (String, String, String, String) in
25 | return (firstName, lastName, username, password)
26 | }
27 |
28 | isValid = userInputs
29 | .map { firstName, lastName, username, password in
30 | return firstName.count > 0 && lastName.count > 0 && username.count > 0 && password.count > 0
31 | }
32 |
33 | let signingUp = ActivityIndicator()
34 | self.signingUp = signingUp.asDriver()
35 |
36 | signedUp = Driver.merge(bindings.signupTaps, bindings.doneTaps)
37 | .withLatestFrom(userInputs)
38 | .flatMap { (arg) -> Driver in
39 | let (firstName, lastName, username, password) = arg
40 | return dependency.userManager.signup(firstName: firstName, lastName: lastName, username: username,
41 | password: password)
42 | .trackActivity(signingUp)
43 | .asDriver(onErrorJustReturn: false)
44 | }
45 |
46 | cancelled = bindings.cancelTaps
47 | }
48 |
49 | // MARK: - ViewModelType
50 |
51 | typealias Dependency = HasUserManager
52 |
53 | struct Bindings {
54 | let firstName: Driver
55 | let lastName: Driver
56 | let login: Driver
57 | let password: Driver
58 | let cancelTaps: Driver
59 | let signupTaps: Driver
60 | let doneTaps: Driver
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Todos/TodosCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodosCoordinator.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | class TodosCoordinator: BaseCoordinator {
12 | typealias Dependencies = HasTodoService
13 |
14 | private let navigationController: UINavigationController
15 | private let dependencies: Dependencies
16 |
17 | init(navigationController: UINavigationController, dependencies: Dependencies) {
18 | self.navigationController = navigationController
19 | self.dependencies = dependencies
20 | }
21 |
22 | override func start() -> Observable {
23 | let viewController = TodosListViewController.instance()
24 | navigationController.viewControllers = [viewController]
25 |
26 | let avm: Attachable = .detached(dependencies)
27 | let viewModel = viewController.attach(wrapper: avm)
28 |
29 | viewModel.selectedTodo
30 | .drive(onNext: { selection in
31 | print("Selected: \(selection)")
32 | })
33 | .disposed(by: viewController.disposeBag)
34 |
35 | // View will never be dismissed
36 | return Observable.never()
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Todos/TodosList/TodosListViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Todos/TodosList/TodosListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodosListViewController.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxCocoa
11 | import RxSwift
12 |
13 | final class TodosListViewController: TableViewController, ViewModelAttaching {
14 |
15 | var viewModel: Attachable!
16 | var bindings: TodosListViewModel.Bindings {
17 | let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
18 | .mapToVoid()
19 | .asDriverOnErrorJustComplete()
20 | let refresh = tableView.refreshControl!.rx
21 | .controlEvent(.valueChanged)
22 | .asDriver()
23 |
24 | return TodosListViewModel.Bindings(
25 | fetchTrigger: Driver.merge(viewWillAppear, refresh),
26 | selection: tableView.rx.itemSelected.asDriver()
27 | )
28 | }
29 |
30 | // MARK: - Lifecycle
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 | setupView()
35 | }
36 |
37 | // MARK: - View Methods
38 |
39 | private func setupView() {
40 | title = "Todos"
41 | }
42 |
43 | func bind(viewModel: TodosListViewModel) -> TodosListViewModel {
44 | viewModel.todos
45 | .drive(tableView.rx.items(cellIdentifier: "Cell")) { _, element, cell in
46 | cell.textLabel?.text = element.title
47 | }
48 | .disposed(by: disposeBag)
49 |
50 | viewModel.fetching
51 | .drive(tableView.refreshControl!.rx.isRefreshing)
52 | .disposed(by: disposeBag)
53 |
54 | viewModel.errors
55 | .delay(.milliseconds(100))
56 | .map { $0.localizedDescription }
57 | .drive(errorAlert)
58 | .disposed(by: disposeBag)
59 |
60 | return viewModel
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Scenes/Todos/TodosList/TodosListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodosListViewModel.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/28/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import RxCocoa
10 | import RxSwift
11 |
12 | final class TodosListViewModel: ViewModelType {
13 |
14 | let fetching: Driver
15 | let todos: Driver<[Todo]>
16 | let selectedTodo: Driver
17 | let errors: Driver
18 |
19 | // MARK: - Lifecycle
20 |
21 | init(dependency: Dependency, bindings: Bindings) {
22 | let activityIndicator = ActivityIndicator()
23 | let errorTracker = ErrorTracker()
24 |
25 | todos = bindings.fetchTrigger
26 | .flatMapLatest {
27 | return dependency.todoService.getTodos()
28 | .trackActivity(activityIndicator)
29 | .trackError(errorTracker)
30 | .asDriverOnErrorJustComplete()
31 | }
32 |
33 | fetching = activityIndicator.asDriver()
34 | errors = errorTracker.asDriver()
35 | selectedTodo = bindings.selection
36 | .withLatestFrom(self.todos) { (indexPath, todos: [Todo]) -> Todo in
37 | return todos[indexPath.row]
38 | }
39 | }
40 |
41 | // MARK: - ViewModelType
42 |
43 | typealias Dependency = HasTodoService
44 |
45 | struct Bindings {
46 | let fetchTrigger: Driver
47 | let selection: Driver
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Services/APIClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIClient.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import CodableAlamofire
11 | import RxSwift
12 |
13 | protocol ClientType {
14 | func request(_: URLRequestConvertible) -> Single
15 | func requestImage(_ endpoint: URLRequestConvertible) -> Single
16 | }
17 |
18 | class APIClient: ClientType {
19 |
20 | // MARK: Properties
21 |
22 | private let sessionManager: SessionManager
23 | private let decoder: JSONDecoder
24 |
25 | private let queue = DispatchQueue(label: "com.mgacy.response-queue", qos: .userInitiated, attributes: [.concurrent])
26 |
27 | // MARK: Lifecycle
28 |
29 | init() {
30 | let configuration = URLSessionConfiguration.default
31 | configuration.timeoutIntervalForRequest = 8 // seconds
32 | configuration.timeoutIntervalForResource = 8 // seconds
33 | sessionManager = Alamofire.SessionManager(configuration: configuration)
34 |
35 | // JSON Decoding
36 | decoder = JSONDecoder()
37 | //decoder.dateDecodingStrategy = .iso8601
38 | }
39 |
40 | // MARK: Methods
41 |
42 | func request(_ endpoint: URLRequestConvertible) -> Single {
43 | return Single.create { [unowned self] single in
44 | let request = self.sessionManager.request(endpoint)
45 | request
46 | .validate()
47 | .responseDecodableObject(queue: self.queue, decoder: self.decoder) { (response: DataResponse) in
48 | switch response.result {
49 | case let .success(val):
50 | single(.success(val))
51 | case let .failure(err):
52 | single(.error(err))
53 | }
54 | }
55 | return Disposables.create {
56 | request.cancel()
57 | }
58 | }
59 | }
60 |
61 | func requestImage(_ endpoint: URLRequestConvertible) -> Single {
62 | return Single.create { [unowned self] single in
63 | let request = self.sessionManager.request(endpoint)
64 | request
65 | .validate()
66 | .responseData { response in
67 | switch response.result {
68 | case .success(let value):
69 | guard let image = UIImage(data: value) else {
70 | single(.error(ClientError.imageDecodingFailed))
71 | return
72 | }
73 | single(.success(image))
74 | case .failure(let err):
75 | single(.error(err))
76 | }
77 | }
78 | return Disposables.create {
79 | request.cancel()
80 | }
81 | }
82 | }
83 |
84 | }
85 |
86 | // MARK: - Errors
87 |
88 | enum ClientError: Error {
89 | case imageDecodingFailed
90 | }
91 |
92 | extension ClientError: LocalizedError {
93 | public var errorDescription: String? {
94 | switch self {
95 | case .imageDecodingFailed:
96 | return "Unable to decode image"
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Services/AlbumService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumService.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 2/28/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import RxSwift
11 |
12 | protocol AlbumServiceType {
13 | func getAlbums() -> Single<[Album]>
14 | func getAlbum(id: Int) -> Single
15 | func getPhotos() -> Single<[Photo]>
16 | func getPhotosFromAlbum(id: Int) -> Single<[Photo]>
17 | func getPhoto(id: Int) -> Single
18 | func getThumbnail(for photo: Photo) -> Single
19 | func getImage(for photo: Photo) -> Single
20 | }
21 |
22 | class AlbumService: AlbumServiceType {
23 | private let client: ClientType
24 |
25 | init(client: ClientType) {
26 | self.client = client
27 | }
28 |
29 | // MARK: Albums
30 |
31 | func getAlbums() -> Single<[Album]> {
32 | return client.request(Router.getAlbums)
33 | }
34 |
35 | func getAlbum(id: Int) -> Single {
36 | return client.request(Router.getAlbum(id: id))
37 | }
38 |
39 | // MARK: Photos
40 |
41 | func getPhotos() -> Single<[Photo]> {
42 | return client.request(Router.getPhotos)
43 | }
44 |
45 | func getPhotosFromAlbum(id: Int) -> Single<[Photo]> {
46 | return client.request(Router.getPhotosFromAlbum(id: id))
47 | }
48 |
49 | func getPhoto(id: Int) -> Single {
50 | return client.request(Router.getPhoto(id: id))
51 | }
52 |
53 | func getThumbnail(for photo: Photo) -> Single {
54 | return client.requestImage(URLRequest(url: photo.thumbnailUrl))
55 | }
56 |
57 | func getImage(for photo: Photo) -> Single {
58 | return client.requestImage(URLRequest(url: photo.url))
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Services/PostService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostService.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 2/28/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import RxSwift
11 |
12 | protocol PostServiceType {
13 | func getPosts() -> Single<[Post]>
14 | func getPost(id: Int) -> Single
15 | }
16 |
17 | class PostService: PostServiceType {
18 | private let client: ClientType
19 |
20 | init(client: ClientType) {
21 | self.client = client
22 | }
23 |
24 | func getPosts() -> Single<[Post]> {
25 | return client.request(Router.getPosts)
26 | }
27 |
28 | func getPost(id: Int) -> Single {
29 | return client.request(Router.getPost(id: id))
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Services/Router.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Router.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 12/27/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 |
11 | public enum Router: URLRequestConvertible {
12 | case getAlbums
13 | case getAlbum(id: Int)
14 | case getPhotos
15 | case getPhoto(id: Int)
16 | case getPhotosFromAlbum(id: Int)
17 | case getPosts
18 | case getPost(id: Int)
19 | case getTodos
20 | case getTodo(id: Int)
21 | case getUser(id: Int)
22 |
23 | static let baseURLString = "https://jsonplaceholder.typicode.com"
24 |
25 | var method: HTTPMethod {
26 | switch self {
27 | default:
28 | return .get
29 | }
30 | }
31 |
32 | var path: String {
33 | switch self {
34 | case .getAlbums:
35 | return "/albums"
36 | case .getAlbum(let id):
37 | return "/albums/\(id)"
38 | case .getPhotos:
39 | return "/photos"
40 | case .getPhoto(let id):
41 | return "/photos/\(id)"
42 | case .getPhotosFromAlbum:
43 | return "/photos"
44 | case .getPosts:
45 | return "/posts"
46 | case .getPost(let id):
47 | return "/posts/\(id)"
48 | case .getTodos:
49 | return "/todos"
50 | case .getTodo(let id):
51 | return "/todos/\(id)"
52 | case .getUser(let id):
53 | return "/users/\(id)"
54 | }
55 | }
56 |
57 | var parameters: Parameters {
58 | switch self {
59 | case .getPhotosFromAlbum(let id):
60 | return ["album": id]
61 | default:
62 | return [:]
63 | }
64 | }
65 |
66 | // MARK: URLRequestConvertible
67 |
68 | public func asURLRequest() throws -> URLRequest {
69 | let url = try Router.baseURLString.asURL()
70 | var urlRequest = URLRequest(url: url.appendingPathComponent(path))
71 | urlRequest.httpMethod = method.rawValue
72 |
73 | switch self {
74 | case .getPhotosFromAlbum:
75 | urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters)
76 | default:
77 | break
78 | }
79 |
80 | return urlRequest
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Services/TodoService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoService.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 2/28/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Alamofire
10 | import RxSwift
11 |
12 | protocol TodoServiceType {
13 | func getTodos() -> Single<[Todo]>
14 | func getTodo(id: Int) -> Single
15 | }
16 |
17 | class TodoService: TodoServiceType {
18 | private let client: ClientType
19 |
20 | init(client: ClientType) {
21 | self.client = client
22 | }
23 |
24 | func getTodos() -> Single<[Todo]> {
25 | return client.request(Router.getTodos)
26 | }
27 |
28 | func getTodo(id: Int) -> Single {
29 | return client.request(Router.getTodo(id: id))
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Services/UserManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserManager.swift
3 | // MVVMC-SplitViewController
4 | //
5 | // Created by Mathew Gacy on 1/1/18.
6 | // Copyright © 2018 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 | import RxCocoa
12 |
13 | enum AuthenticationState {
14 | case signedIn
15 | case signedOut
16 | }
17 |
18 | class UserManager {
19 | var authenticationState: AuthenticationState
20 | let currentUser = BehaviorSubject(value: nil)
21 | private let client: ClientType
22 | private let storageManager: UserStorageManagerType
23 |
24 | init() {
25 | self.client = APIClient()
26 | self.storageManager = UserStorageManager()
27 | if let user = storageManager.read() {
28 | self.authenticationState = .signedIn
29 | self.currentUser.onNext(user)
30 | } else {
31 | self.authenticationState = .signedOut
32 | }
33 | }
34 |
35 | }
36 |
37 | // MARK: - Authentication Services
38 |
39 | typealias AuthenticationService = LoginService & LogoutService & SignupService
40 |
41 | enum AuthenticationError: Error {
42 | case invalidCredentials
43 | }
44 |
45 | extension AuthenticationError: LocalizedError {
46 | public var errorDescription: String? {
47 | switch self {
48 | case .invalidCredentials:
49 | return "Invalid credentials"
50 | }
51 | }
52 | }
53 |
54 | // MARK: LoginService
55 |
56 | protocol LoginService {
57 | func login(username: String, password: String) -> Single
58 | }
59 |
60 | extension UserManager: LoginService {
61 |
62 | func login(username: String, password: String) -> Single {
63 | // just a mock
64 | let loginResult = arc4random() % 5 == 0 ? false : true
65 | if loginResult == false {
66 | return .error(AuthenticationError.invalidCredentials)
67 | }
68 |
69 | return client.request(Router.getUser(id: 1))
70 | .do(onSuccess: { [weak self] (user: User) in
71 | self?.authenticationState = .signedIn
72 | self?.currentUser.onNext(user)
73 | self?.storageManager.store(user: user)
74 | })
75 | .map { _ in return true }
76 | }
77 |
78 | }
79 |
80 | // MARK: LogoutService
81 |
82 | protocol LogoutService {
83 | func logout() -> Single
84 | }
85 |
86 | extension UserManager: LogoutService {
87 |
88 | func logout() -> Single {
89 | // just a mock
90 | return Single.just(true)
91 | .delay(.milliseconds(500), scheduler: MainScheduler.instance)
92 | .do(onSuccess: { [weak self] _ in
93 | self?.authenticationState = .signedOut
94 | self?.storageManager.clear()
95 | self?.currentUser.onNext(nil)
96 | })
97 | }
98 |
99 | }
100 |
101 | // MARK: SignupService
102 |
103 | protocol SignupService {
104 | func signup(firstName: String, lastName: String, username: String, password: String) -> Single
105 | }
106 |
107 | extension UserManager: SignupService {
108 |
109 | func signup(firstName: String, lastName: String, username: String, password: String) -> Single {
110 | // just a mock
111 | let signupResult = arc4random() % 5 == 0 ? false : true
112 | if signupResult == false {
113 | return .error(AuthenticationError.invalidCredentials)
114 | }
115 |
116 | return client.request(Router.getUser(id: 1))
117 | .do(onSuccess: { [weak self] (user: User) in
118 | self?.authenticationState = .signedIn
119 | self?.currentUser.onNext(user)
120 | self?.storageManager.store(user: user)
121 | })
122 | .map { _ in return true }
123 | }
124 |
125 | }
126 |
127 | // MARK: - Persistence
128 |
129 | protocol UserStorageManagerType {
130 | func store(user: User)
131 | func read() -> User?
132 | func clear()
133 | }
134 |
135 | class UserStorageManager: UserStorageManagerType {
136 | private let encoder: JSONEncoder
137 | private let archiveURL: URL
138 |
139 | init() {
140 | encoder = JSONEncoder()
141 | archiveURL = UserStorageManager.getDocumentsURL().appendingPathComponent("user")
142 | }
143 |
144 | func store(user: User) {
145 | // should incorporate better error handling
146 | do {
147 | let data = try encoder.encode(user)
148 | guard NSKeyedArchiver.archiveRootObject(data, toFile: archiveURL.path) else {
149 | fatalError("Could not store data to url")
150 | }
151 | } catch {
152 | fatalError(error.localizedDescription)
153 | }
154 | }
155 |
156 | func read() -> User? {
157 | if let data = NSKeyedUnarchiver.unarchiveObject(withFile: archiveURL.path) as? Data {
158 | let decoder = JSONDecoder()
159 | do {
160 | let user = try decoder.decode(User.self, from: data)
161 | return user
162 | } catch {
163 | fatalError(error.localizedDescription)
164 | }
165 | } else {
166 | return nil
167 | }
168 | }
169 |
170 | func clear() {
171 | // should incorporate better error handling
172 | do {
173 | try FileManager.default.removeItem(at: archiveURL)
174 | } catch {
175 | fatalError("Could not delete data from url")
176 | }
177 | }
178 |
179 | // MARK: - Helper Methods
180 |
181 | private static func getDocumentsURL() -> URL {
182 | if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
183 | return url
184 | } else {
185 | // should incorporate better error handling
186 | fatalError("Could not retrieve documents directory")
187 | }
188 | }
189 |
190 | }
191 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewController/Supporting Files/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 | NSAppTransportSecurity
24 |
25 | NSExceptionDomains
26 |
27 | placehold.it
28 |
29 | NSIncludesSubdomains
30 |
31 | NSTemporaryExceptionAllowsInsecureHTTPLoads
32 |
33 |
34 |
35 |
36 | UILaunchStoryboardName
37 | LaunchScreen
38 | UIRequiredDeviceCapabilities
39 |
40 | armv7
41 |
42 | UISupportedInterfaceOrientations
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 | UISupportedInterfaceOrientations~ipad
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationPortraitUpsideDown
52 | UIInterfaceOrientationLandscapeLeft
53 | UIInterfaceOrientationLandscapeRight
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewControllerTests/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 |
--------------------------------------------------------------------------------
/MVVMC-SplitViewControllerTests/MVVMC_SplitViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MVVMC_SplitViewControllerTests.swift
3 | // MVVMC-SplitViewControllerTests
4 | //
5 | // Created by Mathew Gacy on 12/21/17.
6 | // Copyright © 2017 Mathew Gacy. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import MVVMC_SplitViewController
11 |
12 | // swiftlint:disable type_name
13 |
14 | class MVVMC_SplitViewControllerTests: XCTestCase {
15 |
16 | override func setUp() {
17 | super.setUp()
18 | // Put setup code here. This method is called before the invocation of each test method in the class.
19 | }
20 |
21 | override func tearDown() {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | super.tearDown()
24 | }
25 |
26 | func testExample() {
27 | // This is an example of a functional test case.
28 | // Use XCTAssert and related functions to verify your tests produce the correct results.
29 | }
30 |
31 | func testPerformanceExample() {
32 | // This is an example of a performance test case.
33 | self.measure {
34 | // Put the code you want to measure the time of here.
35 | }
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | platform :ios, '12.0'
3 |
4 | # Ignore all warnings from all pods
5 | inhibit_all_warnings!
6 |
7 | # Basic
8 | def basic_pods
9 | pod 'Alamofire', '~> 4.4'
10 | pod 'CodableAlamofire'
11 | pod 'ColorCompatibility'
12 | # pod 'SwiftyBeaver'
13 | pod 'RxSwift', '~> 5.0'
14 | pod 'RxCocoa', '~> 5.0'
15 | pod 'RxSwiftExt', '~> 5.0'
16 | pod 'RxDataSources', '~> 4.0'
17 | end
18 |
19 | # Testing
20 | def test_pods
21 | pod 'RxBlocking', '~> 5.0'
22 | pod 'RxTest', '~> 5.0'
23 | end
24 |
25 | target 'MVVMC-SplitViewController' do
26 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
27 | use_frameworks!
28 |
29 | # Pods for MVVMC-SplitViewController
30 | basic_pods
31 |
32 | target 'MVVMC-SplitViewControllerTests' do
33 | inherit! :search_paths
34 | # Pods for testing
35 | # test_pods
36 | end
37 |
38 | post_install do |installer|
39 | installer.pods_project.targets.each do |target|
40 |
41 | # Fix RxSwift error
42 | # https://stackoverflow.com/a/75729977/4472195
43 | target.build_configurations.each do |config|
44 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
45 | end
46 |
47 | # Enable RxSwift.Resources for debugging
48 | if target.name == 'RxSwift'
49 | target.build_configurations.each do |config|
50 | if config.name == 'Debug'
51 | config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES']
52 | end
53 | end
54 | end
55 | end
56 | end
57 |
58 | end
59 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Alamofire (4.9.1)
3 | - CodableAlamofire (1.1.2):
4 | - Alamofire (~> 4.0)
5 | - ColorCompatibility (1.0.0)
6 | - Differentiator (4.0.1)
7 | - RxCocoa (5.1.1):
8 | - RxRelay (~> 5)
9 | - RxSwift (~> 5)
10 | - RxDataSources (4.0.1):
11 | - Differentiator (~> 4.0)
12 | - RxCocoa (~> 5.0)
13 | - RxSwift (~> 5.0)
14 | - RxRelay (5.1.1):
15 | - RxSwift (~> 5)
16 | - RxSwift (5.1.1)
17 | - RxSwiftExt (5.2.0):
18 | - RxSwiftExt/Core (= 5.2.0)
19 | - RxSwiftExt/RxCocoa (= 5.2.0)
20 | - RxSwiftExt/Core (5.2.0):
21 | - RxSwift (~> 5.0)
22 | - RxSwiftExt/RxCocoa (5.2.0):
23 | - RxCocoa (~> 5.0)
24 | - RxSwiftExt/Core
25 |
26 | DEPENDENCIES:
27 | - Alamofire (~> 4.4)
28 | - CodableAlamofire
29 | - ColorCompatibility
30 | - RxCocoa (~> 5.0)
31 | - RxDataSources (~> 4.0)
32 | - RxSwift (~> 5.0)
33 | - RxSwiftExt (~> 5.0)
34 |
35 | SPEC REPOS:
36 | trunk:
37 | - Alamofire
38 | - CodableAlamofire
39 | - ColorCompatibility
40 | - Differentiator
41 | - RxCocoa
42 | - RxDataSources
43 | - RxRelay
44 | - RxSwift
45 | - RxSwiftExt
46 |
47 | SPEC CHECKSUMS:
48 | Alamofire: 85e8a02c69d6020a0d734f6054870d7ecb75cf18
49 | CodableAlamofire: b7adfb2e5e980392014b4b3f3b8b62c01b9de4aa
50 | ColorCompatibility: 2762fea4195ed88bfb15eeca3ca5de73bb4a9380
51 | Differentiator: 886080237d9f87f322641dedbc5be257061b0602
52 | RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601
53 | RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114
54 | RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9
55 | RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178
56 | RxSwiftExt: 4ca80336f43c28f11a2825cdd2fc61dd6c044697
57 |
58 | PODFILE CHECKSUM: 3c1c308934ce6e7a3e045736a55f2d602d3ead96
59 |
60 | COCOAPODS: 1.13.0
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MVVMC-SplitViewController
2 |
3 | ### Description
4 | Example project with UITabBarController inside UISplitViewController using RxSwift and MVVM-C architecture.
5 |
6 | Loads example data from [JSONPlaceholder](https://jsonplaceholder.typicode.com/)
7 |
8 |
9 | ### Coordinators
10 |
11 | 
12 |
13 |
14 | ### Screenshots
15 |
16 | 
17 |
18 | 
19 |
20 | ### Requirements
21 |
22 | - Xcode 12
23 | - Swift 5
24 | - iOS 12
25 |
26 | ### Links
27 |
28 | - [Coordinators Redux](http://khanlou.com/2015/10/coordinators-redux/)
29 | - [Reactive MVVM](http://www.thomasvisser.me/2017/02/09/mvvm-rx/)
30 | - [Taming Great Complexity: MVVM, Coordinators and RxSwift](https://blog.uptech.team/taming-great-complexity-mvvm-coordinators-and-rxswift-8daf8a76e7fd)
31 | - [CleanArchitectureRxSwift](https://github.com/sergdort/CleanArchitectureRxSwift)
32 |
--------------------------------------------------------------------------------