Last active
June 26, 2021 02:26
-
-
Save Neko3000/68a1f6b24b47498d3ad17d7fde50e894 to your computer and use it in GitHub Desktop.
Coordinator
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 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 | |
} | |
} |
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 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() | |
} | |
} |
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 { | |
// 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... | |
} |
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 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 | |
} | |
} | |
} |
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 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() | |
} | |
} |
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
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)) | |
} | |
}() |
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 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> | |
} |
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 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