Skip to content

Instantly share code, notes, and snippets.

@AvatarHurden
Last active August 13, 2018 02:37
Show Gist options
  • Save AvatarHurden/52b942e57352c1e7a312abd52dccb20e to your computer and use it in GitHub Desktop.
Save AvatarHurden/52b942e57352c1e7a312abd52dccb20e to your computer and use it in GitHub Desktop.
Coordinator with navigation and service functions
@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
}
}
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
}
}
}
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")
}
}
}
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(())
}
}
}
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)
}
}()
}
@tmbiOS
Copy link

tmbiOS commented Jul 26, 2018

Hi, Arthur!
Can you provide all code of Coordinator.swift, code Scene.swift and some example of how to implement it in AppDelegate, how you create and pass services?
Do you use it when root VC is UITabBarController?

@AvatarHurden
Copy link
Author

Hi @tmbiOS!
I updated the gist with the code for Scene.swift and AppDelegate. For creating and passing services, I made ViewModel depend on FirstServiceProtocol, showing how you could go about passing services. Since most services should be stateless, it doesn't matter that the Coordinator makes a new instance every time. If the service requires state, however, you could make a constant instance in the Coordinator (or anywhere else).

In regards to using the Coordinator for a UITabBarController, 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 multiple Coordinators. 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. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment