화면 전환을 처리하는 Scene Coordinator

import Foundation
enum TransitionStyle {
case root //첫번째 화면 표시
case push
case modal
}
enum TransitionError: Error {
case navigationControllerMissing
case cannotPop
case unknown
}

import UIKit
enum Scene {
case list(MemoListViewModel)
case detail(MemoDetailViewModel)
case compse(MemoComposeViewModel)
}
extension Scene {
//스토리보드에 있는 씬 생성. 연관값에 저장된 뷰모델 바인딩 후 리턴
func instantiate(from storyboard: String = "Main") -> UIViewController {
let storyboard = UIStoryboard(name: storyboard, bundle: nil)
switch self {
case .list(let memoListViewModel):
guard let nav = storyboard.instantiateViewController(withIdentifier: "ListNav") as? UINavigationController else {
fatalError()
}
guard var listVC = nav.viewControllers.first as? MemoListViewController else {
fatalError()
}
listVC.bind(viewModel: memoListViewModel)
return nav
case .detail(let memoDetailViewModel):
guard var detailVC = storyboard.instantiateViewController(withIdentifier: "DetailVC") as? MemoDetailViewController else {
fatalError()
}
detailVC.bind(viewModel: memoDetailViewModel)
return detailVC
case .compse(let memoComposeViewModel):
guard let nav = storyboard.instantiateViewController(withIdentifier: "ComposeNav") as? UINavigationController else {
fatalError()
}
guard var composeVC = nav.viewControllers.first as? MemoComposeViewController else {
fatalError()
}
composeVC.bind(viewModel: memoComposeViewModel)
return nav
}
}
}

import Foundation
import RxSwift
protocol SceneCoordinatorType {
@discardableResult
func transition(to scene: Scene, using style: TransitionStyle, animated: Bool) -> Completable
@discardableResult
func close(animated: Bool) -> Completable
}
import Foundation
import RxSwift
import RxCocoa
class SceneCoordinator: SceneCoordinatorType {
private let bag = DisposeBag()
//화면전환을 처리하기 때문에 Window인스턴스와 현재 화면에 표시되어있는 Scene을 가지고 있어야 함
private var window: UIWindow
private var currentVC: UIViewController
required init(window: UIWindow) {
self.window = window
currentVC = window.rootViewController!
}
@discardableResult
func transition(to scene: Scene, using style: TransitionStyle, animated: Bool) -> RxSwift.Completable {
let subject = PublishSubject<Never>() //성공과 실패만 방출하므로 Never 사용
let target = scene.instantiate()
switch style {
case .root:
currentVC = target
window.rootViewController = target
subject.onCompleted()
case .push:
guard let nav = currentVC.navigationController else {
subject.onError(TransitionError.navigationControllerMissing)
break
}
nav.pushViewController(target, animated: animated)
currentVC = target
subject.onCompleted()
case .modal:
currentVC.present(target, animated: animated) {
subject.onCompleted()
}
currentVC = target
}
return subject.asCompletable()
}
@discardableResult
func close(animated: Bool) -> RxSwift.Completable {
return Completable.create { [unowned self] completable in
if let presentingVC = self.currentVC.presentingViewController {
self.currentVC.dismiss(animated: animated) {
self.currentVC = presentingVC
completable(.completed)
}
}
else if let nav = self.currentVC.navigationController {
guard nav.popViewController(animated: animated) != nil else {
completable(.error(TransitionError.cannotPop))
return Disposables.create()
}
self.currentVC = nav.viewControllers.last!
completable(.completed)
}
else {
completable(.error(TransitionError.unknown))
}
return Disposables.create()
}
}
}