[iOS] RIBs Tutorial 1

hyun·2023년 12월 16일
0

튜토리얼에 앞서 RIBs 프로젝트를 다운받고, 터미널에서 RIBs/ios/tooling 경로로 이동한다. 해당 경로에서

./install-xcode-template.sh

를 입력하여 템플릿을 설치한다.

템플릿에 맞게 Router, ViewController, Builder, Interactor가 생성된다.

https://github.com/uber/RIBs/tree/main/ios/tutorials/tutorial1 과정대로 진행하여, TicTacToe.xcworkspace를 생성한다.

목표

RIB이 상호작용하고 통신하는 방식 이해

프로젝트 구조

  • 두개의 RIB(Root, LoggedOut)으로 구성
  • 앱이 실행되면 해당 AppDelegate는 Root RIB을 구축하고, 애플리케이션에 대한 제어권이 Root RIB으로 전달된다.
  1. Root
    • Root RIB의 목적: RIB 트리의 루트 역할, 필요할때 제어권을 하위 RIB으로 전달
    • Root RIB은 Xcode 템플릿에 의해 자동생성된다.
  2. LoggedOut
    • 로그인 인터페이스, 인증 관련 이벤트 관리
  • Root RIB이 AppDelegate에서 앱에 대한 제어권을 얻으면, LoggedOut RIB으로 제어권을 전송하여 로그인 UI를 표시한다.
  • LoggedOut RIB 구축을 담당하는 코드는 RootRouter에서 제공한다.
// RootRouter.swift

import RIBs

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

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

final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, RootRouting {

    init(interactor: RootInteractable,
         viewController: RootViewControllable,
         loggedOutBuilder: LoggedOutBuildable) {
        self.loggedOutBuilder = loggedOutBuilder
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }

    override func didLoad() {
        super.didLoad()

        let loggedOut = loggedOutBuilder.build(withListener: interactor)
        self.loggedOut = loggedOut
        attachChild(loggedOut)
        viewController.present(viewController: loggedOut.viewControllable)
    }

    // MARK: - Private

    private let loggedOutBuilder: LoggedOutBuildable

    private var loggedOut: ViewableRouting?
}

TicTacToe 프로젝트에는 LoggedOut RIB 구현이 되어있지 않은데, 실제로 구현해보자.

템플릿을 활용해 LoggedOut RIB을 생성한다.

코드 이해

  • LoggedOutBuilder: LoggedOutBuildable 프로토콜 채택
protocol LoggedOutBuildable: Buildable {
    func build(withListener listener: LoggedOutListener) -> LoggedOutRouting
}

final class LoggedOutBuilder: Builder<LoggedOutDependency>, LoggedOutBuildable {

    override init(dependency: LoggedOutDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: LoggedOutListener) -> LoggedOutRouting {
        let component = LoggedOutComponent(dependency: dependency)
        let viewController = LoggedOutViewController()
        let interactor = LoggedOutInteractor(presenter: viewController)
        interactor.listener = listener
        return LoggedOutRouter(interactor: interactor, viewController: viewController)
    }
}
  • LoggedOutInteractor
    • Router와 통신하기 위해서 LoggedOutRouting 프로토콜 채택
      (Interactor가 필요한 것을 선언하고, LoggedOuttedRouter가 구현하는 형태인 의존성 역전(dependency inversion) 원칙에 기반한다.)
      (-> LoggedOutRouting 프로토콜 채택한다는 것은 Buildable 프로토콜과 채택한 것과 유사하게 유닛테스트를 가능하게 해준다.)
    • Interactor가 viewController와 통신하기 위해서 LoggedOutPresentable 프로토콜을 채택한다.
protocol LoggedOutRouting: ViewableRouting {
    // TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
}

protocol LoggedOutPresentable: Presentable {
    var listener: LoggedOutPresentableListener? { get set }
    // TODO: Declare methods the interactor can invoke the presenter to present data.
}

protocol LoggedOutListener: AnyObject {
    // TODO: Declare methods the interactor can invoke to communicate with other RIBs.
}

final class LoggedOutInteractor: PresentableInteractor<LoggedOutPresentable>, LoggedOutInteractable, LoggedOutPresentableListener {

    weak var router: LoggedOutRouting?
    weak var listener: LoggedOutListener?

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    override init(presenter: LoggedOutPresentable) {
        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.
    }
}
  • LoggedOutRouter: LoggedOutInteractable 프로토콜을 채택하여 interactor와 통신하기 위해 필요한 것을 선언한다. LoggedOutViewControllable 프로토콜을 채택하여 viewController와 통신한다.
protocol LoggedOutInteractable: Interactable {
    var router: LoggedOutRouting? { get set }
    var listener: LoggedOutListener? { get set }
}

protocol LoggedOutViewControllable: ViewControllable {
    // TODO: Declare methods the router invokes to manipulate the view hierarchy.
}

final class LoggedOutRouter: ViewableRouter<LoggedOutInteractable, LoggedOutViewControllable>, LoggedOutRouting {

    // TODO: Constructor inject child builder protocols to allow building children.
    override init(interactor: LoggedOutInteractable, viewController: LoggedOutViewControllable) {
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
}
  • LoggedOutViewController: 의존성 역전 원칙을 따르며, interactor와 통신하기 위해 LoggedOutPresenableListenr 프로토콜을 채택한다.

Login Logic


위와 같은 UI 구성은 완료된 상태에서

로그인 버튼이 눌렸을 때,
LoginViewController가 listener(=LoggedOutPresentableListener, 즉, Interactor)에게 사용자가 로그인을 시도한다는 것을 알리고, 두명의 플레이어 이름을 전달하는 로직을 구현하려고 한다.

해당 로직을 구현하기 위해서 먼저 LoggedOutPresentableListener 프로토콜에 함수 하나를 선언하자.

protocol LoggedOutPresentableListener: class {
    func login(withPlayer1Name player1Name: String?, player2Name: String?)
}

그리고 LoggedOutInteractor에 아래의 메소드를 추가한다.

// 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)")
}

private func playerName(_ name: String?, withDefaultName defaultName: String) -> String {
    if let name = name {
        return name.isEmpty ? defaultName : name
    } else {
        return defaultName
    }
}

마지막으로, viewController에서 listener(=Interactor) 메소드를 호출할 수 있도록 연결해준다.

@objc private func didTapLoginButton() {
    listener?.login(withPlayer1Name: player1Field?.text, player2Name: player2Field?.text)
}

정리

참고
https://github.com/uber/RIBs/wiki/iOS-Tutorial-1

0개의 댓글