Last active
August 13, 2018 02:37
-
-
Save AvatarHurden/52b942e57352c1e7a312abd52dccb20e to your computer and use it in GitHub Desktop.
Coordinator with navigation and service functions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@UIApplicationMain | |
class AppDelegate: UIResponder, UIApplicationDelegate { | |
var window: UIWindow? | |
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { | |
// Override point for customization after application launch. | |
let window = UIWindow(frame: UIScreen.main.bounds) | |
self.window = window | |
window.rootViewController = UIViewController() | |
window.makeKeyAndVisible() | |
let coordinator = Coordinator(window: window) | |
let vmParent = ParentViewModel(coordinator: coordinator) | |
let scene = Scene.parent(viewModel: vmParent) | |
coordinator.transition(with: .root(scene: scene)) | |
return true | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RxSwift | |
enum Transition { | |
case root(scene: Scene) | |
case push(scene: Scene, animated: Bool) | |
case modal(scene: Scene, animated: Bool) | |
case pop(animated: Bool) | |
case popped | |
} | |
protocol CoordinatorProtocol { | |
var currentScene: Scene { get } | |
var sceneStack: [Scene] { get } | |
func getService<ServiceProtocol>(type: ServiceProtocol.Type) -> ServiceProtocol | |
@discardableResult | |
func transition(with transition: Transition) -> Observable<Void> | |
} | |
class Coordinator: CoordinatorProtocol { | |
private let window: UIWindow | |
var currentScene: Scene { | |
return Scene(from: self.currentViewController) | |
} | |
var viewStack: [(UIViewController, Transition)] = [] | |
var sceneStack: [Scene] { | |
return viewStack.map { Scene(from: $0.0) } | |
} | |
func getService<ServiceProtocol>(type: ServiceProtocol.Type) -> ServiceProtocol { | |
var service: ServiceProtocol? | |
if ServiceProtocol.self == FirstServiceProtocol.self { | |
service = FirstService() as? ServiceProtocol | |
} else if ServiceProtocol.self == SecondServiceProtocol.self { | |
service = SecondService() as? ServiceProtocol | |
} else { | |
fatalError("Coordinator has no service for the protocol \(ServiceProtocol.self)") | |
} | |
if let service = service { | |
return service | |
} else { | |
fatalError("Service provided by coordinator is not the same as passed as argument (\(ServiceProtocol.self)") | |
} | |
} | |
var currentViewController: UIViewController | |
func actualViewController(for vc: UIViewController) -> UIViewController { | |
if let navigationController = vc as? UINavigationController { | |
return navigationController.viewControllers.first! | |
} else { | |
return vc | |
} | |
} | |
init(window: UIWindow) { | |
self.window = window | |
self.currentViewController = window.rootViewController! | |
} | |
@discardableResult | |
func transition(with transition: Transition) -> Observable<Void> { | |
switch transition { | |
case let .push(scene, animated): | |
let vc = scene.viewController() | |
guard let nav = currentViewController.navigationController else { | |
fatalError("Push requires a navigation controller") | |
} | |
nav.pushViewController(vc, animated: animated) | |
self.currentViewController = self.actualViewController(for: vc) | |
self.viewStack.append((self.currentViewController, transition)) | |
case .popped: | |
if let (_, type) = self.viewStack.popLast(), | |
case .push(_) = type, | |
let (view, _) = self.viewStack.last { | |
self.currentViewController = self.actualViewController(for: view) | |
} | |
// Other cases | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum Scene { | |
case parent(viewModel: ParentViewModelProtocol) | |
case child(viewModel: ChildViewModelProtocol) | |
} | |
extension Scene { | |
func viewController() -> UIViewController { | |
switch self { | |
case let .parent(viewModel): | |
var vc = ParentViewController() | |
vc.bind(to: viewModel) | |
return vc | |
case let .child(viewModel): | |
var vc = ChildViewController() | |
vc.bind(to: viewModel) | |
return vc | |
} | |
init(from viewController: UIViewController) { | |
if let vc = viewController as? ParentViewController { | |
self = .parent(viewModel: vc.viewModel) | |
} else if let vc = viewController as? ChildViewController { | |
self = .child(viewModel: vc.viewModel) | |
} else { | |
fatalError("View Controller \(viewController) is not associated with any scene") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ChildViewController: UIViewController, Bindable { | |
typealias ViewModel = ChildViewModel | |
var viewModel: ChildViewModel! | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
} | |
func bindToViewModel() { | |
} | |
override func viewDidAppear(_ animated: Bool) { | |
super.viewDidAppear(animated) | |
// Must be done after view appeared, since it has no navigationController on loading | |
self.navigationController?.delegate = self | |
} | |
extension ChildViewController: UINavigationControllerDelegate { | |
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { | |
if viewController is ParentViewController { | |
// Only calls on didShow, since willShow is called when the user starts a swipe from the left edge (even if he then cancels the swipe) | |
self.viewModel.returnToParent.execute(()) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ViewModel { | |
private let coordinator: CoordinatorProtocol | |
private let firstService: FirstServiceProtocol | |
init(coordinator: CoordinatorProtocol) { | |
self.coordinator = coordinator | |
self.firstService = coordinator.getService(type: FirstServiceProtocol.type) | |
} | |
lazy var returnToParent: CocoaAction = { | |
return CocoaAction { [weak self] _ in | |
guard let strongSelf = self else { return .empty() } | |
return strongSelf.coordinator.transition(with: .popped) | |
} | |
}() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi @tmbiOS!
I updated the gist with the code for
Scene.swift
andAppDelegate
. For creating and passing services, I madeViewModel
depend onFirstServiceProtocol
, showing how you could go about passing services. Since most services should be stateless, it doesn't matter that theCoordinator
makes a new instance every time. If the service requires state, however, you could make a constant instance in theCoordinator
(or anywhere else).In regards to using the
Coordinator
for aUITabBarController
, although it is technically possible, I don't really recommend it. Currently, I don't use this approach anymore, relying instead on one that uses multipleCoordinator
s. This approach is better for making complex screen flows and is easier to make changes to. There are many sources online for how to implement this, such as here and here.If, however, you really want to use this approach, let me know and I can create a gist with the full code. Be advised, though, that it's quite a beast. 😄