RIBs with NavigationController

hyun·2023년 12월 24일
0

Dependency

  • Xcode 14.3.1
  • uber/RIBs Templates
  • CocoaPods

구조도

Root RIB에서 Navigation RIB을 full Screen으로 present하고, Navigation RIB에서 A, B, C RIB으로는 Navigation 방식으로 화면 전환을 한다. 모두 viewable RIB으로 설정하였다!

SceneDelegate 설정

메인 스토리보드를 제거해준 다음, 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 프로퍼티를 생성해야한다.

Root RIB

  • RootBuilder
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으로 수정하였다.

  • RootRouter
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을 해야한다.

  • Extensions/UINavigationController+
import UIKit
import RIBs

// MARK: RIBs
extension UINavigationController: ViewControllable {
    public var uiviewController: UIViewController {
        return self
    }
    
    convenience init(root: ViewControllable) {
        self.init(rootViewController: root.uiviewController)
    }
}
  • RootViewController
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 방식으로 변경하였다.

  • NavigationBuilder
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한다.

  • NavigationRouter
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으로 전환될때 내비게이션 푸시 방식으로 전환되도록 한다.

  • NavigationViewController

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 함수를 정의한다.

  • NavigationInteractor
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

0개의 댓글