Root RIB에서 Navigation RIB을 full Screen으로 present하고, Navigation RIB에서 A, B, C RIB으로는 Navigation 방식으로 화면 전환을 한다. 모두 viewable RIB으로 설정하였다!
메인 스토리보드를 제거해준 다음, uber튜토리얼과 달리 SceneDelegate에서 rootRouter를 연결시켜준다.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
// MARK: - Private
private var rootRouter: LaunchRouting?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let rootRouter = RootBuilder(dependency: AppComponent()).build()
self.rootRouter = rootRouter
rootRouter.launch(from: window)
}
}
이때, SceneDelegate에 router 프로퍼티를 만들지 않으면, leak이 생기면서 앱이 종료되기 때문에 반드시 router 프로퍼티를 생성해야한다.
protocol RootDependency: Dependency {
// TODO: Declare the set of dependencies required by this RIB, but cannot be
// created by this RIB.
}
final class RootComponent: Component<RootDependency> {
// TODO: Declare 'fileprivate' dependencies that are only used by this RIB.
}
// MARK: - Builder
protocol RootBuildable: Buildable {
func build() -> LaunchRouting
}
final class RootBuilder: Builder<RootDependency>, RootBuildable {
override init(dependency: RootDependency) {
super.init(dependency: dependency)
}
func build() -> LaunchRouting {
let component = RootComponent(dependency: dependency)
let viewController = RootViewController()
let interactor = RootInteractor(presenter: viewController)
let navigationBuilder = NavigationBuilder(dependency: component)
return RootRouter(interactor: interactor, viewController: viewController, navigationBuilder: navigationBuilder)
}
}
RootBuilable 내의 build 함수 파라미터 수정 및 RootRouting -> LaunchRouting으로 수정하였다.
import RIBs
protocol RootInteractable: Interactable, NavigationListener {
var router: RootRouting? { get set }
var listener: RootListener? { get set }
}
protocol RootViewControllable: ViewControllable {
func present(viewController: ViewControllable)
func dismiss(viewController: ViewControllable)
}
final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, RootRouting {
init(interactor: RootInteractable, viewController: RootViewControllable, navigationBuilder: NavigationBuildable) {
self.navigationBuilder = navigationBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
override func didLoad() {
super.didLoad()
routeToNavigation()
}
// MARK: - Private
private let navigationBuilder: NavigationBuildable
private var navigation: NavigationRouting?
private func routeToNavigation() {
let navigation = navigationBuilder.build(withListener: interactor)
self.navigation = navigation
attachChild(navigation)
let navigationController = UINavigationController(root: navigation.viewControllable)
viewController.present(viewController: navigationController)
}
}
Navigation RIB의 viewController를 UINavigationController로 감싸서 present하였다.
위 작업을 위해 UINavigation Extension을 해야한다.
import UIKit
import RIBs
// MARK: RIBs
extension UINavigationController: ViewControllable {
public var uiviewController: UIViewController {
return self
}
convenience init(root: ViewControllable) {
self.init(rootViewController: root.uiviewController)
}
}
import RIBs
import RxSwift
import UIKit
protocol RootPresentableListener: AnyObject {
// TODO: Declare properties and methods that the view controller can invoke to perform
// business logic, such as signIn(). This protocol is implemented by the corresponding
// interactor class.
}
final class RootViewController: UIViewController, RootPresentable, RootViewControllable {
weak var listener: RootPresentableListener?
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Method is not supported")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
}
// MARK: - RootViewControllable
func present(viewController: ViewControllable) {
viewController.uiviewController.modalPresentationStyle = .fullScreen
present(viewController.uiviewController, animated: true, completion: nil)
}
func dismiss(viewController: ViewControllable) {
if presentedViewController === viewController.uiviewController {
dismiss(animated: true, completion: nil)
}
}
}
present 방식을 modal 방식에서 fullScreen 방식으로 변경하였다.
protocol NavigationDependency: Dependency {
// TODO: Declare the set of dependencies required by this RIB, but cannot be
// created by this RIB.
}
final class NavigationComponent: Component<NavigationDependency> {
// TODO: Declare 'fileprivate' dependencies that are only used by this RIB.
}
// MARK: - Builder
protocol NavigationBuildable: Buildable {
func build(withListener listener: NavigationListener) -> NavigationRouting
}
final class NavigationBuilder: Builder<NavigationDependency>, NavigationBuildable {
override init(dependency: NavigationDependency) {
super.init(dependency: dependency)
}
func build(withListener listener: NavigationListener) -> NavigationRouting {
let component = NavigationComponent(dependency: dependency)
let viewController = NavigationViewController()
let interactor = NavigationInteractor(presenter: viewController)
interactor.listener = listener
let aBuilder = NavigationABuilder(dependency: component)
return NavigationRouter(interactor: interactor, viewController: viewController, navigationABuilder: aBuilder )
}
}
Navigation RIB은 UINavigationController가 하던 역할을 그대로 수행한다. A RIB을 root view controller로 설정하고, A RIB으로부터 listener를 통해 결과를 전달받으면 B RIB을 push한다.
import RIBs
protocol NavigationInteractable: Interactable, NavigationAListener {
var router: NavigationRouting? { get set }
var listener: NavigationListener? { get set }
}
protocol NavigationViewControllable: ViewControllable {
func push(viewController: ViewControllable)
}
final class NavigationRouter: ViewableRouter<NavigationInteractable, NavigationViewControllable>, NavigationRouting {
// TODO: Constructor inject child builder protocols to allow building children.
init(interactor: NavigationInteractable, viewController: NavigationViewControllable, navigationABuilder: NavigationABuildable) {
self.navigationABuilder = navigationABuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
private let navigationABuilder: NavigationABuildable
private var navigationA: NavigationARouting?
func routeToNavigationA() {
let navigationA = navigationABuilder.build(withListener: interactor)
self.navigationA = navigationA
attachChild(navigationA)
viewController.push(viewController: navigationA.viewControllable)
}
}
NavigationViewControllable에 push 함수를 정의하여 A RIB으로 전환될때 내비게이션 푸시 방식으로 전환되도록 한다.
protocol NavigationPresentableListener: AnyObject {
func didTappedNavigationAButton()
}
final class NavigationViewController: UIViewController, NavigationPresentable, NavigationViewControllable {
weak var listener: NavigationPresentableListener?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.purple
buildRouterAButton()
}
// MARK: - NavigationViewControllable
func push(viewController: ViewControllable) {
uiviewController.navigationController?.pushViewController(viewController.uiviewController, animated: true)
}
// MARK: - Private
private func buildRouterAButton() {
let aButton = UIButton()
view.addSubview(aButton)
aButton.snp.makeConstraints { (maker: ConstraintMaker) in
maker.centerX.centerY.equalToSuperview()
}
aButton.setTitle("navigation TO A", for: .normal)
aButton.setTitleColor(UIColor.white, for: .normal)
aButton.backgroundColor = UIColor.black
aButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.listener?.didTappedNavigationAButton()
})
.disposed(by: disposeBag)
}
private let disposeBag = DisposeBag()
}
NavigationViewControllable 프로토콜의 push 함수를 정의한다.
import RIBs
import RxSwift
protocol NavigationRouting: ViewableRouting {
func routeToNavigationA()
}
protocol NavigationPresentable: Presentable {
var listener: NavigationPresentableListener? { get set }
// TODO: Declare methods the interactor can invoke the presenter to present data.
}
protocol NavigationListener: AnyObject {
// TODO: Declare methods the interactor can invoke to communicate with other RIBs.
}
final class NavigationInteractor: PresentableInteractor<NavigationPresentable>, NavigationInteractable, NavigationPresentableListener {
weak var router: NavigationRouting?
weak var listener: NavigationListener?
// TODO: Add additional dependencies to constructor. Do not perform any logic
// in constructor.
override init(presenter: NavigationPresentable) {
super.init(presenter: presenter)
presenter.listener = self
}
override func didBecomeActive() {
super.didBecomeActive()
// TODO: Implement business logic here.
}
override func willResignActive() {
super.willResignActive()
// TODO: Pause any business logic.
}
// MARK: - NavigationPresentableListener
func didTappedNavigationAButton() {
router?.routeToNavigationA()
}
}
NavigationViewController로부터 A RIB으로 푸시 이벤트가 들어오면 listener를 통해 결과를 전달받고 router로 푸시 이벤트를 전달한다.
참고
https://velog.io/@frankjinhan/RIBs-Flattening
https://minsone.github.io/programming/swift-ribs-viewcontrollable-extension