Skip to content

Instantly share code, notes, and snippets.

@Neko3000
Last active June 26, 2021 02:26
Show Gist options
  • Save Neko3000/68a1f6b24b47498d3ad17d7fde50e894 to your computer and use it in GitHub Desktop.
Save Neko3000/68a1f6b24b47498d3ad17d7fde50e894 to your computer and use it in GitHub Desktop.
Coordinator
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
// Manually creates the window and makes it visible.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = UIViewController() // Dummy VC for Coordinator's init
let sceneCoordinator = SceneCoordinator(window: window!)
let firstSceneViewModel = FirstSceneViewModel(coordinator: sceneCoordinator)
let firstScene = Scene.firstScene(firstSceneViewModel)
sceneCoordinator.transition(to: firstScene, type: .root)
return true
}
}
import UIKit
import RxSwift
protocol BindableType {
associatedtype ViewModelType
var viewModel: ViewModelType! { get set }
func bindViewModel()
}
extension BindableType where Self: UIViewController {
mutating func bindViewModel(to model: Self.ViewModelType) {
viewModel = model
loadViewIfNeeded()
bindViewModel()
}
}
import Foundation
enum Scene {
// Sub-group of scenes related to each other
// E.g.: all scenes part of a login process
case firstScene(FirstSceneViewModel)
case secondScene(SecondSceneViewModel)
// Another sub-group of scenes related to each other
// An so on...
}
import UIKit
extension Scene {
func viewController() -> UIViewController {
switch self {
case .firstScene(let viewModel):
let nc = UINavigationController(rootViewController: FirstSceneViewController())
var vc = nc.viewControllers.first as! FirstSceneViewController
vc.bindViewModel(to: viewModel)
return nc
case .secondScene(let viewModel):
var vc = SecondSceneViewController()
vc.bindViewModel(to: viewModel)
return vc
}
}
}
import UIKit
import RxSwift
import RxCocoa
final class SceneCoordinator: SceneCoordinatorType {
fileprivate var window: UIWindow
var currentViewController: UIViewController
required init(window: UIWindow) {
self.window = window
currentViewController = window.rootViewController!
}
static func actualViewController(for viewController: UIViewController) -> UIViewController {
if let navigationController = viewController as? UINavigationController {
return navigationController.viewControllers.first!
} else {
return viewController
}
}
@discardableResult
func transition(to scene: Scene, type: SceneTransitionType) -> Observable<Void> {
let subject = PublishSubject<Void>()
let viewController = scene.viewController()
switch type {
case .root:
currentViewController = SceneCoordinator.actualViewController(for: viewController)
window.rootViewController = viewController
subject.onCompleted()
case .push(let animated):
guard let navigationController = currentViewController.navigationController else {
fatalError("Can't push a view controller without a current navigation controller")
}
// one-off subscription to be notified when push complete
_ = navigationController.rx.delegate
.sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
.map { _ in }
.bind(to: subject)
navigationController.pushViewController(viewController, animated: animated)
currentViewController = SceneCoordinator.actualViewController(for: viewController)
case .modal(let animated):
currentViewController.present(viewController, animated: animated) {
subject.onCompleted()
}
currentViewController = SceneCoordinator.actualViewController(for: viewController)
case .pushToVC(let stack, let animated):
guard let navigationController = currentViewController.navigationController else {
fatalError("Can't push a view controller without a current navigation controller")
}
var controllers = navigationController.viewControllers
stack.forEach { controllers.append($0) }
controllers.append(viewController)
// one-off subscription to be notified when push complete
_ = navigationController.rx.delegate
.sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
.map { _ in }
.bind(to: subject)
navigationController.setViewControllers(controllers, animated: animated)
currentViewController = SceneCoordinator.actualViewController(for: viewController)
default:
break
}
return subject.asObservable()
.take(1)
.ignoreElements()
}
@discardableResult
func pop(animated: Bool) -> Observable<Void> {
let subject = PublishSubject<Void>()
if let presenter = currentViewController.presentingViewController {
// dismiss a modal controller
currentViewController.dismiss(animated: animated) {
self.currentViewController = SceneCoordinator.actualViewController(for: presenter)
subject.onCompleted()
}
} else if let navigationController = currentViewController.navigationController {
// navigate up the stack
// one-off subscription to be notified when pop complete
_ = navigationController.rx.delegate
.sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
.take(1) // To delete if already in return at bottom
.map { _ in }
.bind(to: subject)
guard navigationController.popViewController(animated: animated) != nil else {
fatalError("can't navigate back from \(currentViewController)")
}
currentViewController = SceneCoordinator.actualViewController(for: navigationController.viewControllers.last!)
} else {
fatalError("Not a modal, no navigation controller: can't navigate back from \(currentViewController)")
}
return subject.asObservable()
.take(1)
.ignoreElements()
}
@discardableResult
func popToRoot(animated: Bool) -> Observable<Void> {
let subject = PublishSubject<Void>()
if let navigationController = currentViewController.navigationController {
// navigate up the stack
// one-off subscription to be notified when pop complete
_ = navigationController.rx.delegate
.sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
.take(1) // To delete if already in return at bottom
.map { _ in }
.bind(to: subject)
guard navigationController.popToRootViewController(animated: animated) != nil else {
fatalError("can't navigate back to root VC from \(currentViewController)")
}
currentViewController = SceneCoordinator.actualViewController(for: navigationController.viewControllers.first!)
}
return subject.asObservable()
.take(1)
.ignoreElements()
}
@discardableResult
func popToVC(_ viewController: UIViewController, animated: Bool) -> Observable<Void> {
let subject = PublishSubject<Void>()
if let navigationController = currentViewController.navigationController {
// navigate up the stack
// one-off subscription to be notified when pop complete
_ = navigationController.rx.delegate
.sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
.take(1) // To delete if already in return at bottom
.map { _ in }
.bind(to: subject)
guard navigationController.popToViewController(viewController, animated: animated) != nil else {
fatalError("can't navigate back to VC from \(currentViewController)")
}
currentViewController = SceneCoordinator.actualViewController(for: navigationController.viewControllers.last!)
}
return subject.asObservable()
.take(1)
.ignoreElements()
}
}
lazy var pushScene: CocoaAction = {
return Action { [weak self] in
guard let strongSelf = self else { return .empty() }
// The ViewModel is created and its dependencies are injected
let newSceneViewModel = NewSceneViewModel(service: NewSceneService(), coordinator: strongSelf.coordinator)
// A reference to the corresponding scene is created to be passed to the coordinator
let newScene = Scene.newScene(newSceneViewModel)
// The coordinator calls the specified transition function and returns an Observable<Void>
// that will complete once the transition is made (one `Void` element will be pushed onto the
// Observable)
return strongSelf.coordinator.transition(to: newScene, type: .push(animated: true))
}
}()
import UIKit
import RxSwift
protocol SceneCoordinatorType {
init(window: UIWindow)
var currentViewController: UIViewController { get }
@discardableResult
func transition(to scene: Scene, type: SceneTransitionType) -> Observable<Void>
// pop scene from navigation stack or dismiss current modal
@discardableResult
func pop(animated: Bool) -> Observable<Void>
@discardableResult
func popToRoot(animated: Bool) -> Observable<Void>
@discardableResult
func popToVC(_ viewController: UIViewController, animated: Bool) -> Observable<Void>
}
import UIKit
enum SceneTransitionType {
case root
case push(animated: Bool)
case modal(animated: Bool)
// Add custom transtion types...
case pushToVC(stackPath: [UIViewController], animated: Bool)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment