iOS

iOS) RIBs Tutorial 2

snowe 2022. 1. 13. 17:29

Tutorial 1 내용 간단 요약

Goal

-RIB에 존재하는 Interactor, Builder, Router, Presenter등이 서로 어떻게 커뮤니케이션 하는지 이해하기

 

 


 

Official Wiki
 

GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.

Uber's cross-platform mobile architecture framework. - GitHub - uber/RIBs: Uber's cross-platform mobile architecture framework.

github.com

소스코드
 

GitHub - snowedev/Study-RIBs: Practice Architecture(RIBs)

Practice Architecture(RIBs). Contribute to snowedev/Study-RIBs development by creating an account on GitHub.

github.com


 

Tutorial 2

Goal

  • 상위RIB과 소통하는 하위RIB 만들기
  • 상위RIB의 Interactor가 결정하는 대로 하위RIB을 attaching / detaching 해보기
  • view-less RIB 만들기
    • view-less RIB이 detach될 때 view의 변경 정리하기
  • RIB의 life cycle 이해하기
    • 상위 RIB이 처음 로드될 때 하위 RIB attach하기
  • RIB unit testing

Project Structure

이전의 튜토리얼을 완료했다면 RootLoggedOut, 두개의 RIB으로 구성된 트리가 있을거에요. 이번 튜토리얼에서는 LoggedIn, OffGame 그리고 TicTacToe 라는 세가지 추가적인 RIB을 트리에 붙일겁니다. 완성될 트리 구조는 아래와 같습니다.

여기서 LoggedIn RIB은 view-less입니다. 이 RIB은 오로지 TicTacToe와 OffGame RIB사이의 전환에 대한 역할만을 담당합니다. 

 

나머지 RIB들은 자신만의 뷰컨과 스크린에 보여줄 뷰들을 가지고 있습니다. 

OffGame RIB은 플레이어가 새로운 게임을 시작할 수 있게 해줍니다. 그래서 "Start Geme" 버튼을 가질 것입니다.

TicTacToe RIB은 게임 영역을 보여줄 것이며 플레이어가 이동을 할 수 있도록 해줄 것입니다.

 


Communicating with a parent RIB

제일 먼저 뭘 할거냐면, 상위 RIB과 하위 RIB간의 소통을 구현해볼거에요

 

사용자가 플레이어 이름 텍스트필드에 이름을 입력한 뒤 로그인 버튼을 탭하면 게임을 시작할 수 있게 해주어야 하잖아요. 그래서 현재 뷰에서 "Start Game" 뷰로 이동되도록 할거에요. 이걸 위해서 현재 활성화 되어있는 LoggedOut RIB은 Root RIB에게 로그인 액션에 대해 알려주어야 합니다. 이를 통해 Root Router는 LoggedOut RIB에서 LoggedIn RIB으로 전환제어를 하게 됩니다.  이어서, view-less RIB인 LoggedIn RIB은 OffGame RIB을 불러올 것이고 그 뷰를 뷰 컨트롤러에 띄울거에요.

 

대략적인 흐름은 이렇게 되는데 이제 어떤식으로 LoggedOutRoot가 소통할 것인지 알아볼게요.

 

Root RIB이 LoggedOut RIB의 상위 RIB이기 때문에, Root의 Router는 LoggedOut Interactor의 리스너가 됩니다. 우리는 이 리스너를 통해 로그인 이벤트를 LoggedOut RIB에서 Root RIB으로 전달할거에요

 

위와 같은 역할을 위해서 LoggedOutListener를 다음과 같이 업데이트 해주세요

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

 

이제 요 리스너를 채택하게 되면 LoggedOut RIB의 상위 RIB은 강제적으로 didLogin을 구현해야하고  컴파일러가 이를 감시합니다. 

 

이제 리스너를 만들었으니 LoggedOut RIB에서 login 액션이 발동됐을 때 리스너를 호출함으로써 상위 RIB에게 알려주어야 겠네요.

LoggedOutInteractor 내부에 있는 login 메서드의 구현부를 바꿔줄게요.

func login(withPlayer1Name player1Name: String?, player2Name: String?) {
    let player1NameWithDefault = playerName(player1Name, withDefaultName: "Player 1")
    let player2NameWithDefault = playerName(player2Name, withDefaultName: "Player 2")
    listener?.didLogin(withPlayer1Name: player1NameWithDefault, player2Name: player2NameWithDefault)
}

이제 LoggedOut RIB의 리스너는 사용자가 "Login" 버튼을 탭하면 호출될거에요

 


Routing to LoggedIn RIB

처음에 보았던 RIB 트리에서 볼 수 있지만, 사용자가 로그인을 하고 나면(리스너를 통해 그걸 인지하고 나면) Root RIB은 LoggedOut RIB -> LoggedIn RIB 으로 전환을 시켜주어야 해요. 

 

RootRouting 프로토콜에 LoggedIn RIB으로 라우팅 해주기 위한 코드를 작성할게요

// MARK: Root/RootInteractor.swift

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

이 코드를 통해 RootInteractorRootRouter간의 계약이 성립되었어요

 

그리고 RootInteractor에서 LoggedOutListner 프로토콜에 생성한 메서드인 didLogin의 body를 아래와 같이 작성해줄게요.

// MARK: Root/RootInteractor.swift

final class RootInteractor: PresentableInteractor<RootPresentable>, RootInteractable, RootPresentableListener {
    func didLogin(withPlayerName player1Name: String, player2Name: String) {
        router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
    }
    // ...
  }

 

지금까지 작성한 코드를 요약하자면,

이제 LoggedOut RIB에서 로그인 버튼을 탭하면 LoggedOutListener 프로토콜에 의해 LoggedOut의 상위 RIB(Root RIB)에서 didLogin 메서드가 호출이 될거고, router의 routeToLoggedIn을 호출할거에요.

 

이를 통해 유저가 로그인을 하게 되면, Root RIB이 LoggedIn RIB으로 라우팅 할 수 있게 됩니다. 하지만 우리는 아직 LoggedIn RIB을 가지고 있지 않기 때문에 Root RIB에서 전환이 될 수 없어요. 얼른 구현하러 갑시당

 

튜토리얼의 LoggedIn 그룹에서 DELETE\_ME.swift 파일을 지우고, LoggedIn 폴더에서 RIB 템플릿을 선택하여 LoggedIn RIB을 만듭시다("Owns corresponding view"는 체크 해제 해주세요 요놈은 view-less RIB이니까요).

 

그러면 LoggedInRouter, LoggedInBuilder, LoggedInInteractor 라는 세가지의 파일이 생성됩니다.


Attaching a viewless LoggedIn RIB and detaching LoggedOut RIB when the users log in

새롭게 생성된 RIB을 attach 하기 위해서, RootRouter는 그걸 빌드할 수 있어야 합니다. LoggedInBuildable 프로토콜을 생성자 주입을 통해 RootRouter에 전달하여 이를 가능하게 합니다. RootRouter의 코드를 다음과 같이 수정할게요.

// MARK: Root/RootRouter.swift
init(interactor: RootInteractable,
     viewController: RootViewControllable,
     loggedOutBuilder: LoggedOutBuildable,
     loggedInBuilder: LoggedInBuildable) { // new
    self.loggedOutBuilder = loggedOutBuilder
    self.loggedInBuilder = loggedInBuilder // new
    super.init(interactor: interactor, viewController: viewController)
    interactor.router = self
}

// MARK: - Private

private let loggedInBuilder: LoggedInBuilder // new

...

 

그리고나서, LoggedIntBuilder 구체 클래스를 인스턴스화 하고 그걸 RootRouter에 주입할 수 있게  RootBuilder를 업데이트 할게요. RootBuilderbuild() 메서드를 수정해주세요.

// MARK: Router/RootBuilder.swift

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) // new
    return RootRouter(interactor: interactor,
                      viewController: viewController,
                      loggedOutBuilder: loggedOutBuilder,
                      loggedInBuilder: loggedInBuilder) // new
}

방금 수정한 코드를 보면 생성자 주입을 사용하여 LoggedInBuilder에 대한 종속성으로 RootComponent를 전달하고 있어요. 이 부분에 대해서는 튜토리얼 3에서 다룬다고 하네여!

 

RootRouterLoggedInBuilder 클래스 대신 LoggedInBuildable 프로토콜에 의존합니다.  이를 통해 RootRouter를 단위 테스트할 때 LoggedInBuildable에 대한 테스트 모의를 전달할 수 있습니다. 동시에 이는 프로토콜 기반 프로그래밍 원칙을 따르므로 RootRouterLoggedInBuilder가 밀접하게 결합되지 않도록 합니다.

 

이제 LoggedIn RIB에 대한 상용구 코드를 전부 생성했고 Root RIB이 LoggedIn RIB을 인스턴스화 할 수 있습니다. 이제 RootRouter 안에 routeToLoggedIn 메서드를 구현해야합니다. 

 

detach를 하려면 dismiss가 필요하겠네요, RootRouter에  RootViewControllable 프로토콜 내에 아래처럼 dismiss를 추가해주세요.

// MARK: Root/RootRouter.swift
protocol RootViewControllable: ViewControllable {
    func present(viewController: ViewControllable)
    func dismiss(viewController: ViewControllable)
}

저 프로토콜을 준수하고 있는 RootViewController에서 dismiss에 대한 구현부도 작성해주어야겠죠?

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

 

이제 본격적으로 detach/attach 부분을 작성해볼게요. RootRouter에 다음의 코드를 새롭게 추가해주세요

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

위 코드에서 볼 수 있듯 새로운 하위 RIB으로 전환하려면, 상위 RIB은 먼저 지금 붙어있는 하위 RIB을 detach 해야합니다. 그 다음에 새로운 하위 RIB을 만들고 방금 detach한 자리에 attach 해줍니다. 

 

RIB과 뷰 계층 구조 간의 일관성을 유지하는 것도 상위 RIB의 책임입니다. 만약 하위 RIB이 view controller를 가지고 있다면 상위 RIB은 하위 RIB이 detach 될 때 하위 view controller 또한 dismiss 하거나 present 해주어야합니다. 

 

위의 routeToLoggedn를 통해 자신의 view controller를 가진 RIB을 어떻게 attach하는지 이해해보시면 좋습니다!

 

새롭게 생성된 LoggedIn RIB으로부터 이벤트를 받을 수 있도록 하기 위해, Root RIB은 자신의 interactor를 LoggedIn RIB의 리스너로 구성합니다. 위 코드에서 Root RIB이 하위 RIB을 빌드할 때 그런 일들이 일어나게 되는데, 이 시점에서 아직 루트 RIB는 LoggedIn RIB의 요청에 응답할 수 있는 프로토콜을 구현하지 않습니다.

 

RIB은 프로토콜 기반이므로 리스너 인터페이스를 준수하는것에 엄격합니다. 다른 암시적 관찰 방법 대신 프로토콜을 사용하므로 부모가 자식의 모든 이벤트를 구현하지 않을때 런타임이 아닌 컴파일러에서 오류를 반환하게 됩니다.

 

이제 RootInteractable을 LoggedInBuilder의 빌드 메서드에 대한 리스너로 전달하므로 RootInteractabl은 LoggedInListener 프로토콜을 준수해야 합니다. RootInteractable에 해당 프로토콜을 따르도록 추가해주세요.

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

 

이제, RootRouterrouteToLoggedIn 메서드를 통해 LoggedIn RIB으로 라우팅할 때 LoggedOut RIB을 detach할 수 있고 view controller도 dismiss할 수 있습니다. 


Pass in LoggedInViewControllable instead of creating it

LoggedIn RIB이 자신의 view을 가지고 있지 않지만 여전히 하위 RIB의 뷰들은 보여주어야 할 필요가 있기 때문에, LoggedIn RIB은 view의 조상(상위 view)로의 접근이 가능해야합니다. 튜토리얼에서 LoggedIn RIB의 상위 RIB은 Root RIB이기 때문에 이 조상 뷰 또한 Root RIB에서 제공이 되어야합니다.

 

RootViewController에 아래의 extension을 추가해서 LoggedInViewControllable을 준수하도록 해주세요

// MARK: LoggedInViewControllable

extension RootViewController: LoggedInViewControllable {
}

 

우리는 LoggedinViewControllable 인스턴스를 LoggedIn RIB에 주입 할 필요가 있는데 얘 역시 tutorial 3에서 다룬대요.

지금은 일단 LoggedInBuilder.swift를 이 코드로 채워주면 된다네요

 

여기까지 되면 이제 LoggedIn RIB은 Root RIB에 의해 수행된 LoggedInViewControllable의 메서드를 통해 하위 RIB들을 show/hide 할 수 있게 됩니다.


Attaching the OffGame RIB when the LoggedIn RIB loads

 

앞서 언급했듯 LoggedIn RIB은 view-less 하기 때문에 오로지 자신의 하위 RIB들간의 전환만 가능합니다. 이제 그 하위 RIB들을 만들어볼거에요. "Start Game" 버튼을 가지고있고 이를 관리하는 OffGame이라고 불리는 RIB을 만들어봅시당

 

OffGame폴더 내에 이번에는 뷰를 가진 RIB을 만들어줄게요.

RIB템플릿을 선택하고 "Owns corresponding view"를 체크한 상태로 만들어주세요.

그리고 OffGameViewController이 코드를 추가해주세요.

 

이제 OffGame RIB을 상위 RIB인 LoggedIn RIB과 연결해봅시다. LoggedIn RIB은 OffGame RIB을 자신의 하위 RIB으로서 attach할 수 있어야합니다. 

 

OffGameBuildable 인스턴스에 대한 종속성을 선언하기 위해 LoggedInRouter의 생성자를 변경해야해요.

init(interactor: LoggedInInteractable,
     viewController: LoggedInViewControllable,
     offGameBuilder: OffGameBuildable) { // new
    self.viewController = viewController
    self.offGameBuilder = offGameBuilder //new
    super.init(interactor: interactor)
    interactor.router = self
}

// MARK: - Private

...

private let offGameBuilder: OffGameBuildable // new

 

 

이제 LoggedInBuilder를 수정해서 OffGameBuilder 클래스를 인스턴스화하고 LoggedInRouter 인스턴스에 주입합니다. 다음과 같이 빌드 기능을 수정해주세요.

// MARK: LoggedIn/LoggedInBuilder.swift

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

여기까지하면 에러가 하나 나올텐데.. 이걸 해결하려면 OffGameBuilder의 계약된 종속성을 만족해야합니다. 음 약속을 지켜야한다는 거죠?

 

OffGameBuilder의 계약된 종속성을 만족하기 위해서, 우리는 LoggedInComponent 클래스를 OffGameComponent를 준수할 수 있게 수정해야합니다(RIB 종속성과 컴포넌트들은 튜토리얼 3에서 좀 더 자세하게 다룬대요).

// MARK: LoggedIn/LoggedInComponent.swift

final class LoggedInComponent: Component<LoggedInDependency>, OffGameDependency {
    
    fileprivate var loggedInViewController: LoggedInViewControllable {
        return dependency.loggedInViewController
    }
}

 

사용자가 로그인 한 뒤 즉각적으로 OffGame RIB에 의한 시작 화면이 나오면 참 좋겠죠? 그래서 LoggedIn RIB은 자신이 로드 되자마자 OffGame RIB을 attach해주어야 해요.

 

LoggedInRouter에서 didLoad 메서드를 오버라이드 해서 OffGame RIB을 바로 띄울 수 있도록 로드해주세요

// MARK: LoggedIn/LoggedInRouter.swift


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

override func didLoad() {
    super.didLoad()
    attachOffGame()
}

...

 

이러면 이제 Argument type 'LoggedInInteractable' does not conform to expected type 'OffGameListener' 라는 에러가 뜨것죠?

 

해주면 되죠 LoggedInInteractableOffGameListener 프로토콜을 준수하도록 해줄게요. 이걸 준수해주어야 OffGame RIB 이벤트를 받을 수 있게 됩니다.

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

 

이제, LoggedIn RIB은 로딩이 된 후에 바로 OffGame RIB을 attach할 수 있고, OffGame에서 오는 이벤트들을 들을 수 있습니다.


Cleaning up the attached views when the LoggedIn RIB is detached

LoggedIn RIB이 자신의 뷰를 가지고 있지 않고, 오히려 상위 계층 뷰를 수정하기 때문에 Root RIB는 LoggedIn RIB가 수행했을 수 있는 뷰의 수정을 자동으로 제거할 방법이 없습니다.

 

다행히 Viewless LoggedIn RIB을 생성하는 데 사용한 Xcode 템플릿은 LoggedIn RIB이 분리될 때 뷰 수정 사항을 정리할 수 있는 후크를 이미 제공합니다.(다행이다)

 

LoggedInViewControllable 프로토콜에 present, dismiss 메서드를 만들어주세요

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

그리고 나서 우리는 cleanupViews라는 메서드가 현재 자식RIB의 view controller를 dismiss 해줄 수 있도록 해줄거에요.

LoggedInRouter의 cleanupViews 메서드를 다음과 같이 바꿔주세요

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

cleanupViews 메소드는 상위 RIB이 LoggedIn RIB를 detach 할 때 LoggedInInteractor에 의해 호출됩니다. cleanupViews에서 현재 보여지고 있는 뷰 컨트롤러를 해제함으로써 LoggedIn RIB이 분리된 후 상위 RIB의 뷰 계층 구조에 뷰를 남기지 않도록 보장합니다.


Switching to TicTacToe RIB on tapping "Start Game" button

지금까지 했던 거랑 비슷하니까 혼자 해보라는데..TicTacToe 자체의 UI는 이미 튜토리얼 프로젝트에 만들어져있어요.

 

뭘 해주면 될까요  일단 우리가 방금까지 만든 OffGame RIB과 같은 계층에 있는 RIB이니까 마찬가지로 TicTacToe RIB의 상위 RIB은 LoggedIn RIB이겠네요!

 

일단 LoggedIn RIB이 로드되자마자 OffGame RIB을 호출하잖아요. 그리고나서 OffGame RIB에서 Start를 탭 하면 TicTacToe RIB으로 전환해주어야해요.

 

1. 가장 먼저 OffGame RIB의 Start버튼에 대한 TapGesture를 추가해줄게요.

// MARK: OffGame/OffGameViewController.swift

// MARK: - Private
private func buildStartButton() {
	...
    
    startButton.rx.tap								
        .subscribe(onNext: { [weak self] in
            self?.listener?.startTicTacToe()
        })
        .disposed(by: disposeBag)
}

private let disposeBag = DisposeBag() 

...

2. VC에서 Interactor로 전달할 수 있도록 PresentableListener에 메소드를 추가해주세요

// OffGameViewController.swift

protocol OffGamePresentableListener: class {
    func startTicTacToe()
}

3. Listener를 통해 상위 RIB에 이벤트를 전달할 수 있도록 할게요

// OffGameInteractor.swift
...

protocol OffGameListener: AnyObject {
    func startTicTacToe()
}

final class OffGameInteractor: PresentableInteractor<OffGamePresentable>, OffGameInteractable, OffGamePresentableListener {
    
    func startTicTacToe() {
        listener?.attachTicTacToe()
    }
    
    ...
}

4. 이제 상위 RIB(LoggedIn RIB)에서 startTicTacToe에 대한 구현부를 작성하면 돼요

// LoggedInInteractor에서 Routing 메서드 추가

protocol LoggedInRouting: Routing {
    func cleanupViews()
    // TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
    func routeToTicTacToe()
}
// LoggedInRouter.swift

func routeToTicTacToe() {
    attachTicTacToe()
    detachCurrentChild()
}

// MARK: - Private

...

private func attachTicTacToe() {
    let tictactoe = ticTacToeBuilder.build(withListener: interactor)
    self.currentChild = tictactoe
    attachChild(tictactoe)
    viewController.present(viewController: tictactoe.viewControllable)
}

private func detachCurrentChild() {
    if let currentChild = currentChild {
        detachChild(currentChild)
        viewController.dismiss(viewController: currentChild.viewControllable)
        self.currentChild = nil
    }
}

5. 하위 리스너를 상위 RIB Router에 연결해준다

// LoggedInInteractor.swift

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

 

6. TicTacToe Builder의 종속성을 LoggedInRouter의 생성자에 생성해주기

init(interactor: LoggedInInteractable,
     viewController: LoggedInViewControllable,
     offGameBuilder: OffGameBuildable,
     tictactoeBuilder: TicTacToeBuildable) { 		// new

    self.viewController = viewController
    self.offGameBuilder = offGameBuilder
    self.ticTacToeBuilder = tictactoeBuilder		// new

    super.init(interactor: interactor)
    interactor.router = self
}

// MARK: - Private    
...
private let ticTacToeBuilder: TicTacToeBuildable	// new

6-1. LoggedInBuilder를 수정해서 TicTacToe Builder 클래스를 인스턴스화하고 LoggedInRouter 인스턴스에 주입

func build(withListener listener: LoggedInListener) -> LoggedInRouting {
    let component = LoggedInComponent(dependency: dependency)
    let interactor = LoggedInInteractor()
    interactor.listener = listener

    let offGameBuilder = OffGameBuilder(dependency: component)
    let tictactoeBuilder = TicTacToeBuilder(dependency: component)		// new

    return LoggedInRouter(interactor: interactor,
                          viewController: component.loggedInViewController,
                          offGameBuilder: offGameBuilder,
                          tictactoeBuilder: tictactoeBuilder)			// new
}

6-2. TicTacToeBuilder의 계약된 종속성을 만족하기 위해서, 우리는 LoggedInComponent 클래스를 TicTacToeComponent를 준수할 수 있게 수정

// MARK: LoggedIn/LoggedInBuilder.swift

final class LoggedInComponent: Component<LoggedInDependency>, OffGameDependency, TicTacToeDependency {
    
    fileprivate var loggedInViewController: LoggedInViewControllable {
        return dependency.loggedInViewController
    }
}

7. 5번에서 상위 RIB이 TicTacToeListener를 준수하게 되면서 TicTacToe 리스너의 gameDidEnd() 메서드를 구현해야하는데 이건 다음 스텝에서 하는거 같아서 그냥 주석처리 해두면 된다

 

여기까지 완료되면

요렇게 된다!


Attaching the OffGame RIB and detaching the TicTacToe RIB when we have a winner

위의 gif에서 게임이 끝나면 TicTacToe RIB에서 다시 게임을 시작할 수 있는 OffGame RIB 으로 돌아가야합니다.

그러려면 지금 attach 되어있는 TicTacToe RIB를 detach하고 OffGame RIB을 attach 해주어야겠네요.

 

LoggedIn RIB과 <-> OffGame RIB / TicTacToe RIB 사이의 attach,detach를 구현해놨고

지금까지 한것과 동일한 리스너 기반 라우팅 패턴을 사용하면 돼요. 제공된 코드의 TicTacToe RIB에는 이미 리스너가 설정되어 있습니다. LoggedIn RIB가 TicTacToe 이벤트에 응답할 수 있도록 LoggedInInteractor에서 구현하기만 하면 됩니다.

 

LoggedInRouting 프로토콜에 routeToOffGame 메서드를 선언해주세요

// LoggedInInteractor.swift

protocol LoggedInRouting: Routing {
    func routeToTicTacToe()
    func routeToOffGame()
    func cleanupViews()
}

아까 비워두었던 gameDidEnd() 메서드의 바디를 작성할게요

// LoggedInInteractor.swift

final class LoggedInInteractor: Interactor, LoggedInInteractable {
    ...
	
    // MARK: - TicTacToeListener
    
    func gameDidEnd() {
        router?.routeToOffGame()
    }
}

그런 다음, routeToOffGame 메서드를 LoggedInRouter 클래스 안에서 구현해주세요

// LoggedInRouter.swift

final class LoggedInRouter: Router<LoggedInInteractable>, LoggedInRouting {
    ...
    func routeToOffGame() {
        detachCurrentChild()
        attachOffGame()
    }
    ...
}

 

완성