ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RIBs) tutorial2
    iOS 2021. 6. 26. 00:13
    uber 공식 튜토리얼을 따라가면서 제 나름대로 정리한 내용입니다. 
    보다 정확한 내용은 원문을 참고해주시고 오류나 수정사항이 있으면 알려주시면 감사하겠습니다. 🙇🏻‍♂️
    전체 코드를 보시려면 여기로 ;)

     

    ⚠️ 우버 튜토리얼의 순서를 그대로 따라가지 않습니다.
    이전 튜토리얼에서 부터 만들어나갑니다.

     

    목표

    • 자식 RIB과 부모 RIB 통신
    • 부모 interactor에서 자식 RIB attaching/dettaching
    • View-less RIB 생성
      • view-less RIB이 dettaching될 때 cleaning up view 정리
    • 부모 RIB이 처음 로드될 때 자식 RIB attaching
      • RIB 라이프사이클 이해하기
    • RIB Unit testing

     

     

    프로젝트 구조

    그림과 같은 RIB tree를 구성하려고 합니다. uber 튜토리얼에서는 각 튜토리얼 단계마다 템플릿을 제공하는데요, 저는 이게 더 헷갈리더라구요... 뭐가 추가된건지.. 왔다갔다 어질어질...

    그래서 그냥 앞선 tutorial1에서부터 시작해보겠습니다 :)

     

    login 구현

    앞선 튜토리얼에서 loggedOutInteractor에서 login 메소드가 호출되는 것까지 확인해봤습니다.

    이제 실제 로직을 구현해봐야겠죠? 여기서 로그인이라는 건 로그인 화면이 사라지고 LoggedIn 화면이 보여지는 걸 의미합니다. (뒤에 보겠지만 LoggedIn은 view-less RIB이라서 TicTacToe 화면을 띄워줍니다.)

    RIBs의 관점에서 보면 LoggedOut RIB을 제거하고 LoggedIn RIB을 붙이는 과정을 의미합니다 ;)

    로그인이 되면 LoggedOut RIB이 제거되고 LoggedIn RIB이 붙어주기 위해서 이 RIB들의 부모 RIB에서 처리해줘야 합니다. 위 트리에서 보시는 것처럼 Root RIB에서 처리해줘야 하죠.

    따라서 Root에서 이를 이벤트를 확인할 수 있도록 RootInteractable에서 LoggedOutListener를 채택해줍니다. (이미 되어있습니다.) 그리고 LoggedOutListener에 로그인 되었음을 처리할 didLogin 메소드를 추가합니다.

    protocol LoggedOutListener: AnyObject {
        func didLogin(withPlayer1Name player1Name: String, player2Name: String)
    } 

     

    자연스럽게 LoggedOutListener를 채택한 RootInteractable을 채택하는 RootInteractor에 해당 메소드를 구현해줘야 합니다.

    // MARK: - LoggedOutListener
    
    func didLogin(withPlayer1Name player1Name: String, player2Name: String) {
        router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
    }

     

    이제 앞서 LoggedOutInteractor에서 print로 처리했던 부분을 수정해줄 수 있게 되었습니다.

    // MARK: - LoggedOutPresentableListener
    
    func login(withPlayer1Name player1Name: String?, player2Name: String?) {
        let player1NameWithDefault = playerName(player1Name, withDefaultName: "Player 1")
        let player2NameWithDefault = playerName(player2Name, withDefaultName: "Player 2")
        
    		// print("\(player1NameWithDefault) vs \(player2NameWithDefault)")
        listener?.didLogin(withPlayer1Name: player1NameWithDefault, player2Name: player2NameWithDefault)
    }

     

    위 과정을 통해 login버튼이 눌린 이벤트를 Root RIB까지 넘겨줬습니다. 이제 LoggedOut RIB을 떼고 LoggedIn RIB을 붙일 차례입니다. 그 전에 아직 붙일 RIB이 없으니 먼저 만들고 가겠습니다.

     

     

    LoggedIn RIB 생성

    그 전에 아직 LoggedIn RIB이 없으므로 먼저 만들어줘야겠네요. LoggedIn 폴더를 만들고 RIB을 만들어줍니다. LoggedIn RIB은 view-less RIB으로 만들거라서 Own corresponding view 체크를 해제해주고 만들어주세요 :)

    그러면 아래와 같은 RIB 파일들이 생성됩니다.

    이제 Root RIB에서 이 LoggedIn RIB을 생성할 수 있게 해줘야 하겠죠?

    이를 위해 RootRouter에 LoggedIn RIB을 만들 builder가 필요합니다. 만들어주고 생성자를 통해 넘겨받을 수 있게 만들어줍니다.

    init(interactor: RootInteractable,
         viewController: RootViewControllable,
         loggedOutBuilder: LoggedOutBuildable,
         loggedInBuilder: LoggedInBuildable) {
        self.loggedOutBuilder = loggedOutBuilder
        self.loggedInBuilder = loggedInBuilder
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
    
    ... 
    
    // MARK: - Private
    
    private let loggedOutBuilder: LoggedOutBuildable
    private let loggedInBuilder: LoggedInBuildable

    생성자 파라미터가 추가되었으니 RootBuilder에서 LoggedInBuilder를 만들어서 넘겨줍니다.

    근데 문제가 생겼네요! RootComponent가 LoggedInDepency를 따르고 있지 않아서 생기는 에러인데요,

    뒤에 나오겠지만 간단하게 일단은 component는 dependency의 구현체라고 생각하시면 될 것 같습니다. (일단 저는 그렇게 이해했습니다..)

    loggedOutBuilder에 component를 넘겨줄 수 있는 이유는 이미 RootComponent+LoggedOut.swift 파일에서 RootComponent가 LoggedOutDependency를 채택해주고 있기 때문입니다. 그러니 우리도RootComponent+LoggedIn.swift 를 만들어서 채택해줍니다 :)

    카피하기 전에 잠깐! 이렇게 Component를 확장시켜주는 경우도 템플릿으로 제공하고 있습니다 ;)

    Component Extension을 만들어줍니다.

    현재 우리는 Root에서 LoggedIn 자식 RIB을 붙여주려고 하므로 아래와 같이 설정해줍니다.

    이제 RootBuilder에서 에러가 사라졌습니다! 하지만... 또 다른 문제가 생겨버립니다. 😭

    LoggedInDependency를 제대로 따르고 있지 못하다는데요, 즉 아직 덜 구현된 녀석이 남아있다는 말입니다.

    그래서 LoggedInDependency를 따라가보면 LoggedOutDependency에는 없는 녀석이 추가되어 있습니다.

    protocol LoggedInDependency: Dependency {
        // TODO: Make sure to convert the variable into lower-camelcase.
        var loggedInViewController: loggedInViewControllable { get }
        // TODO: Declare the set of dependencies required by this RIB, but won't be
        // created by this RIB.
    }

    네 눈치 빠르신 분들은 이미 아셨을텐데요, LoggedIn RIB은 바로 뷰가 없는 view-less RIB이기 때문입니다. LoggedInViewControllable로 가보면 다음과 같이 얘는 자기 뷰를 가지고 있지 않으니 조상 중에 하나는 내놓으라고 친절히 설명해주고 있습니다.

    따라서 LoggedInViewController에 rootViewController를 리턴해줄 수 있도록 수정해줍니다.

    extension RootComponent: LoggedInDependency {
        
        var LoggedInViewController: LoggedInViewControllable {
            return rootViewController
        }
    }

     

    하지만 지금은 rootViewController가 없으니 RootComponent로 가서 rootViewController를 만들어주겠습니다. rootViewController는 AppDelegate에서 window 부터 내려온 녀석을 사용해야 하므로 처음에 주입받도록 생성자도 같이 만들어줍니다.

    final class RootComponent: Component<RootDependency> {
    
        let rootViewController: RootViewController
    
        init(dependency: RootDependency,
             rootViewController: RootViewController) {
            self.rootViewController = rootViewController
            super.init(dependency: dependency)
        }
    }

     

    이 전에 RootViewController가 LoggedInViewControllable을 따를 수 있도록 해줘야겠죠?

    // MARK: LoggedInViewControllable
    
    extension RootViewController: LoggedInViewControllable { }

    이제 처음 build할 때 뷰컨을 넣어주도록 합니다.

    // RootBuilder
    
    func build() -> LaunchRouting {
        let viewController = RootViewController()
        let component = RootComponent(dependency: dependency, rootViewController: viewController)
        let interactor = RootInteractor(presenter: viewController)
    
        let loggedOutBuilder = LoggedOutBuilder(dependency: component)
        let loggedInBuilder = LoggedInBuilder(dependency: component)
        return RootRouter(interactor: interactor,
                          viewController: viewController,
                          loggedOutBuilder: loggedOutBuilder,
                          loggedInBuilder: loggedInBuilder)
    }

    Routing to LoggedIn RIB

    이제 다시 원래 하려고 했던! login 버튼 이벤트를 받은 Root에서 LoggedOut RIB을 떼고 LoggedIn RIB을 붙여보겠습니다.

    interactor에서 리스너를 채택해서 처리해주니 우선 RootInteractor에서 처리해줘야겠죠? RootInteractor 파일로 가서 Routing 로직을 추가해줍니다.

    protocol RootRouting: ViewableRouting {
        func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String)
    }

    그리고 interactor의 didLogin 에서 호출해줍니다.

    // MARK: - LoggedOutListener
    
    func didLogin(withPlayer1Name player1Name: String, player2Name: String) {
        router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
    }

    이제 router로 가서 진짜 route 로직을 구현해보겠습니다.

    private var loggedOut: ViewableRouting?
    
    // MARK: - RootRouting
    
    func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) {
        // Detach LoggedOut RIB.
        if let loggedOut = self.loggedOut {
            detachChild(loggedOut)
            viewController.dismiss(viewController: loggedOut.viewControllable)
            self.loggedOut = nil
        }
    
        let loggedIn = loggedInBuilder.build(withListener: interactor)
        attachChild(loggedIn)
    }

     

    지금은 위 코드에서 컴파일 에러가 발생할텐데요, 일단 대강 살펴보면 loggedOut 이 있으면 loggedIn 을 붙여주는 모습같죠?

    하나씩 고쳐봅시다. 일단 viewController에 dismiss가 없다고 하네요. 여기서 viewController는 UIViewController가 아닙니다! ViewControllable Procotol이에요. 여긴 root니 RootViewControllable에 dismiss를 만들어줍니다.

    protocol RootViewControllable: ViewControllable {
        func present(viewController: ViewControllable)
        func dismiss(viewController: ViewControllable)
    }

    ViewControllable은 UIViewController에서 채택하고 있습니다. 따라서 뷰컨으로 가서 dismiss를 구현해줍니다.

    func dismiss(viewController: ViewControllable) {
        if presentedViewController === viewController.uiviewController {
            dismiss(animated: true, completion: nil)
        }
    }

    다음으로는 interactor가 LoggedInListener를 채택하고 있지 않아서 넘겨줄 수가 없네요. 추가해줍니다.

    protocol RootInteractable: Interactable, LoggedOutListener, LoggedInListener {
        var router: RootRouting? { get set }
        var listener: RootListener? { get set }
    }

    여기까지 해서 LoggedOut화면이 사라지는 것까지 구현 되었지만 여전히 LoggedIn는 뷰가 존재하지 않습니다. 그저 루트뷰를 보여줄 뿐이죠! LoggedIn은 viewless한 RIB이여서 자식 뷰를 전환시키는 역할만 담당합니다. 이이제 LoggedIn의 자식이 될 RIB을 만들어보겠습니다.

     

     

    LoggedIn이 로드되면 OffGame RIB 붙이기

    이제 RIB 생성은 쉽게 할 수 있으시겠죠? OffGame RIB을 만들어줍니다. 이번에도 uber에서 UI를 제공해주네요. 바로 복.붙.

    import RIBs
    import SnapKit
    import UIKit
    
    protocol OffGamePresentableListener: class {
        // 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 OffGameViewController: UIViewController, OffGamePresentable, OffGameViewControllable {
        var uiviewController: UIViewController {
            return self
        }
    
        weak var listener: OffGamePresentableListener?
    
        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.yellow
            buildStartButton()
        }
    
        // MARK: - Private
    
        private func buildStartButton() {
            let startButton = UIButton()
            view.addSubview(startButton)
            startButton.snp.makeConstraints { (maker: ConstraintMaker) in
                maker.center.equalTo(self.view.snp.center)
                maker.leading.trailing.equalTo(self.view).inset(40)
                maker.height.equalTo(100)
            }
            startButton.setTitle("Start Game", for: .normal)
            startButton.setTitleColor(UIColor.white, for: .normal)
            startButton.backgroundColor = UIColor.black
        }
    }

     

    이번에 해야 할 목적은 LoggedIn RIB이 load되면 OffGame RIB을 만들어서 붙여주는 것입니다. 따라서 이번에도 OffGame을 만들 수 있도록 builder가 필요합니다. loggedInRouter에 builder를 주입 받을 수 있도록 수정해줍니다.

    final class LoggedInRouter: Router<LoggedInInteractable>, LoggedInRouting {
    
        init(interactor: LoggedInInteractable,
             viewController: LoggedInViewControllable,
             offGameBuilder: OffGameBuildable) {
            self.viewController = viewController
            self.offGameBuilder = offGameBuilder
            super.init(interactor: interactor)
            interactor.router = self
        }
    
    ...
    
    // MARK: - Private
    
    ...
    
        private let loggedInBuilder: LoggedInBuildable

    이제 Root에서 LoggedIn 빌더를 주입 해줬을 때처럼 LoggedIn에서 OffGame을 주입시켜줘야 합니다.

    func build(withListener listener: LoggedInListener) -> LoggedInRouting {
        let component = LoggedInComponent(dependency: dependency)
        let interactor = LoggedInInteractor()
        interactor.listener = listener
        
        let offGameBuilder = OffGameBuilder(dependency: component)
        return LoggedInRouter(interactor: interactor,
                              viewController: component.LoggedInViewController,
                              offGameBuilder: offGameBuilder)
    }

     

    역시 겪어봤던 상황입니다. LoggedIn component를 확장시켜서 OffGameDependency를 따르도록 해줍니다.

    이제 OffGame을 만들어서 붙일 수 있는 준비가 되었습니다. 이제 LoggedIn이 load되면 붙여주면 됩니다.

    Router에서는 didLoad 메소드를 제공합니다. 이걸 override해서 attach 해줍니다.

    override func didLoad() {
        super.didLoad()
        attachOffGame()
    }
    
    // MARK: - Private
    
    private var currentChild: ViewableRouting?
    
    private func attachOffGame() {
        let offGame = offGameBuilder.build(withListener: interactor)
        self.currentChild = offGame
        attachChild(offGame)
        viewController.present(viewController: offGame.viewControllable)
    }

     

    역시 이번에도 바로 되지는 않습니다. OffGame의 이벤트를 부모인 LoggedInInteractor에서 처리해줘야 하므로 넘겨주기 전에 OffGameListener를 채택하도록 해줍니다.

    protocol LoggedInInteractable: Interactable, OffGameListener {
        var router: LoggedInRouting? { get set }
        var listener: LoggedInListener? { get set }
    }

     

    그리고 LoggedInViewControllable에 present를 만들어줍니다. 이와 호응되는 dismiss도 함께 정의해주겠습니다.

    protocol LoggedInViewControllable: ViewControllable {
        func present(viewController: ViewControllable)
        func dismiss(viewController: ViewControllable)
    }

    마지막으로 자식 RIB이 detach 될 때 뷰를 정리할 수 있도록 Router의 cleanupViews를 업데이트시켜줍니다.

    func cleanupViews() {
        if let currentChild = currentChild {
            viewController.dismiss(viewController: currentChild.viewControllable)
        }
    }

    여기까지 되셨다면 login 버튼을 눌렀을 때 다음과 화면을 보실 수 있습니다.

     

    아직 2편 끝이 아닌데.. 생각보다 길어져서 여기서 한 번 끊고 가겠습니다.

    'iOS' 카테고리의 다른 글

    RIBs) tutorial3  (0) 2021.06.26
    RIBs) tutorial2-2  (0) 2021.06.26
    RIBs) tutorial1  (0) 2021.06.25
    CALayer 그리고 View와의 관계  (0) 2021.01.31
    Intrinsic Content Size, Content Hugging, Content Compression Resistance  (0) 2021.01.24

    댓글

Designed by Tistory.