-
-
Save AvatarHurden/52b942e57352c1e7a312abd52dccb20e to your computer and use it in GitHub Desktop.
@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) | |
} | |
}() | |
} |
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 Coordinator
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. 😄
Hi, Arthur!
Can you provide all code of
Coordinator.swift
, codeScene.swift
and some example of how to implement it inAppDelegate
, how you create and pass services?Do you use it when root VC is
UITabBarController
?