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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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) | |
| } | |
| }() | |
| } |
Author
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.swiftandAppDelegate. For creating and passing services, I madeViewModeldepend onFirstServiceProtocol, showing how you could go about passing services. Since most services should be stateless, it doesn't matter that theCoordinatormakes 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
Coordinatorfor 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 multipleCoordinators. 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. 😄