로그인 후 게임 필드를 표시한다.
LoggedIn RIB
OffGame RIB
TicTacToe RIB
사용자가 플레이어 이름을 입력하고 Login 버튼을 누르면 게임 시작으로 이동하는 로직을 구현한다고 하자. 이를 위해선,
1. LoggedOut RIB이 Root RIB에 로그인 작업에 대해 알려야한다.
2. Root Router는 제어권을 LoggedOut RIB에서 LoggedIn RIB으로 전환한다.
3. LoggedIn RIB은 view-less이기 때문에 OffGame RIB을 로드하고 해당 viewController를 화면에 표시한다.
Root RIB은 LoggedOut RIB의 상위 RIB으로서, Root RIB의 router는 LoggedOut RIB의 interactor의 listener로 구성된다.
(왜지...? LoggedOutInteractor의 listener는 RootInteractor 아닌가..)
Listener 인터페이스를 통해 LoggedOut RIB의 로그인 이벤트를 Root RIB으로 전달해야한다.
먼저, LoggedOutListener 프로토콜을 아래와 같이 수정한다.
// before
protocol LoggedOutListener: AnyObject {
// TODO: Declare methods the interactor can invoke to communicate with other RIBs.
}
// after
protocol LoggedOutListener: class {
func didLogin(withPlayer1Name player1Name: String, player2Name: String)
}
LoggedOutListener 프로토콜에 메소드를 업데이트 하여, LoggedOut RIB이 Root RIB에 알릴 수 있도록 한다.
LoggedOutInteractor에 LoggedOutListener에 선언한 didLogin 메소드를 호출할 수 있도록
// LoggedOutInteractor.swift
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)
}
login 함수 구현을 수정하였다.
위 구현으로, LoggedOutRIB의 리스너(= 상위 RIB인 Root RIB의 Interactor)는 사용자가 로그인 버튼을 누르면 로그인했다는 알림을 받게 된다.
사용자가 로그인 한후, Root RIB에서 LoggedIn RIB으로 전환하기 위한 routing 코드를 작성해보자.
RootRouting 프로토콜을 아래와 같이 업데이트 한다.
// before
protocol RootRouting: ViewableRouting {
// TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
}
// after
protocol RootRouting: ViewableRouting {
func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String)
}
위 프로토콜을 채택하면, RootInteractor와 Root Router 사이에 계약?(contract)이 설정된다.
🤔 아 그래서.. 위에서 LoggedOutInteractor의 listener는 RootRouter라고 표현한건가?
LoggedIn RIB으로 라우팅 하기 위해서, RootInteractor는 LoggedOutListener를 구현해야한다.
final class RootInteractor: PresentableInteractor<RootPresentable>, RootInteractable, RootPresentableListener {
weak var router: RootRouting?
weak var listener: RootListener?
// TODO: Add additional dependencies to constructor. Do not perform any logic
// in constructor.
override init(presenter: RootPresentable) {
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: - LoggedOutListener
func didLogin(withPlayer1Name player1Name: String, player2Name: String) {
router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
}
}
이렇게 되면 사용자가 로그인할 때마다 Root RIB이 LoggedIn RIB으로 라우팅 된다. (LoggedIn RIB이 구현이 앞으로 된다면)
view-less한 LoginRIB을 구현하기 위해서 Own corresponding view를 체크해제한후, 탬플릿을 생성해준다.
LoggedIn RIB을 연결하기 위해선, Root Router가 LoggedInRIB을 build할 수 있어야한다. LoggedInBuildable 프로토콜을 RootRouter에 주입한다.
이때, RootRouter에서 LoggedInBuilder라는 구체 클래스를 주입하지 않고, LoggedInBuildable라는 프로토콜에 의존한다. 이를 통해서 Mock을 사용한 단위 테스트를 진행할 수 있고, 두 객체의 결합도가 떨어진다.
final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, RootRouting {
init(interactor: RootInteractable,
viewController: RootViewControllable,
loggedOutBuilder: LoggedOutBuildable,
loggedInBuilder: LoggedInBuildable) {
self.loggedOutBuilder = loggedOutBuilder
self.loggedInBuilder = loggedInBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
override func didLoad() {
super.didLoad()
routeToLoggedOut()
}
// MARK: - Private
private let loggedOutBuilder: LoggedOutBuildable
private let loggedInBuilder: LoggedInBuildable
private var loggedOut: ViewableRouting?
private func routeToLoggedOut() {
let loggedOut = loggedOutBuilder.build(withListener: interactor)
self.loggedOut = loggedOut
attachChild(loggedOut)
viewController.present(viewController: loggedOut.viewControllable)
}
}
RootBuilder 클래스를 업데이트하여, LoggedInBuilder 구체 클래스를 생성하고, RootRouter에 주입한다.
final class RootBuilder: Builder<RootDependency>, RootBuildable {
override init(dependency: RootDependency) {
super.init(dependency: dependency)
}
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)
}
}
이제... RootRouter에서 routeToLoggedIn 메소드를 구현하여 LoggedIn RIB으로 연결시켜 보자
// 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)
}
위 코드에서 보면 알 수 있듯이, control을 전환하려면 상위 RIB이 기존 하위 RIB을 detach하고 새 하위 RIB을 생성고 attach해야한다.
RootViewControllable 프로토콜을 아래와 같이 수정하고
protocol RootViewControllable: ViewControllable {
func present(viewController: ViewControllable)
func dismiss(viewController: ViewControllable)
}
RootViewController에 dismiss 메소드를 추가한다.
// RootViewController
func dismiss(viewController: ViewControllable) {
if presentedViewController === viewController.uiviewController {
dismiss(animated: true, completion: nil)
}
}
RootInteractable 프로토콜에 아래와 같이 LoggedInListener를 추가한다.
protocol RootInteractable: Interactable, LoggedOutListener, LoggedInListener {
weak var router: RootRouting? { get set }
weak var listener: RootListener? { get set }
}
위 코드를 통해서 RootRoutersms LoggedOut RIB와 detach되고, routeToLoggedIn 메소드가 호출되서 LoggedIn RIB으로 라우팅될 때 LoggedOutViewController가 dismiss 된다.
LoggedIn RIB에는 뷰가 없지만 하위 RIB의 뷰를 표시할 수 있어야 하기 때문에, LoggedIn RIB은 상위 RIB의 뷰에 연결되야 한다. 따라서, 지금 사용하는 view는 LoggedIn RIB의 상위인 Root RIB으로부터 제공된 것이다.
RootViewController에 아래와 같이 extension을 추가하여 LoogedInViewControllable 프로토콜을 따르도록 한다.
// RootViewController.swift
// MARK: LoggedInViewControllable
extension RootViewController: LoggedInViewControllable {
}
LoggedInViewControllabe을 LoggedIn RIB에 삽입해야하는데, 이 내용은 tutorial3에서 다룰 예정이다. 지금은 LoggedInBuilder 객체를 아래 코드와 같이 수정만 해둔다.
import RIBs
protocol LoggedInDependency: Dependency {
var loggedInViewController: LoggedInViewControllable { get }
}
final class LoggedInComponent: Component<LoggedInDependency> {
fileprivate var loggedInViewController: LoggedInViewControllable {
return dependency.loggedInViewController
}
}
// MARK: - Builder
protocol LoggedInBuildable: Buildable {
func build(withListener listener: LoggedInListener) -> LoggedInRouting
}
final class LoggedInBuilder: Builder<LoggedInDependency>, LoggedInBuildable {
override init(dependency: LoggedInDependency) {
super.init(dependency: dependency)
}
func build(withListener listener: LoggedInListener) -> LoggedInRouting {
let component = LoggedInComponent(dependency: dependency)
let interactor = LoggedInInteractor()
interactor.listener = listener
return LoggedInRouter(interactor: interactor,
viewController: component.loggedInViewController)
}
}
이제, LoggedIn RIB은 LoggedInViewControllable메소드를 호출하여 하위 RIB view를 표시하거나 숨길 수 있다.
LoggedIn RIB의 하위 RIB인 OffGame RIB을 생성해보자.
새로 생성한 OffGame RIB을 상위 RIB인 LoggedIn RIB과 연결시키자.
LoggedIn Router의 생성자를 변경하여 OffGameBuildable 인스턴스를 build한다.
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
}
func cleanupViews() {
// TODO: Since this router does not own its view, it needs to cleanup the views
// it may have added to the view hierarchy, when its interactor is deactivated.
}
// MARK: - Private
private let viewController: LoggedInViewControllable
private let offGameBuilder: OffGameBuildable
}
이제, LoggedInBuilder를 업데이트 하여 OffGameBuilder 구체적인 클래스를 인스턴스화하고 offGameBuilder 인스턴스를 LoggedInRouter에 삽입한다.
final class LoggedInBuilder: Builder<LoggedInDependency>, LoggedInBuildable {
override init(dependency: LoggedInDependency) {
super.init(dependency: dependency)
}
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)
}
}
사용자가 로그인하면, OffGame RIB의 시작화면을 표시하려고 한다. 즉, LoggedIn RIB이 로드되자마자 OffGame RIB이 attach 되야한다. LoggedInRoter의 didLoad 메소드를 override하여 OffGame RIB을 로드하자.
// LoggedInRoter.swift
override func didLoad() {
super.didLoad()
attachOffGame()
}
attachOffGame private method는 offGameBuilder를 생성하고, OffGame RIB을 attach하고, viewController를 띄운다.
// 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)
}
OffGameBuilder를 인스턴스화 할때, LoggedInInteractable 인스턴스를 주입한다. 이 Interactor는 OffGame의 listener 인터페이스로, 하위 RIB으로부터 이벤트가 오면 상위 RIB이 받도록 해준다. (즉, OffGame RIB에서 이벤트가 올 때, LoggedIn RIB에 전달시키기 위해서 사용)
LoggedInInteractable을 아래와 같이 수정한다.
protocol LoggedInInteractable: Interactable {
var router: LoggedInRouting? { get set }
var listener: LoggedInListener? { get set }
}
protocol LoggedInInteractable: Interactable, OffGameListener {
weak var router: LoggedInRouting? { get set }
weak var listener: LoggedInListener? { get set }
}
이제, LoggedInRIB은 OfffGameRIB과 attach되며, offGameRIB의 이벤트를 listen한다.
LoggedIn RIB은 view-less이고 상위 view 계층을 보여준다. 그렇다면 LoggedIn RIB이 detach될때, 연결되어 있던 view(=Root RIB의 View)를 사라지게 하는 방법은 무엇일까?
protocol LoggedInViewControllable: ViewControllable {
func present(viewController: ViewControllable)
func dismiss(viewController: ViewControllable)
}
위 프로토콜을 선언하면, LoggedIn RIB은 viewControllable을 dismiss할 수 있게 된다.
parent RIB에서 LoggedIn RIB을 detach해야겠다는 결정을 내리면 LoggedInRouter의 아래의 함수를 호출한다.
func cleanupViews() {
if let currentChild = currentChild {
viewController.dismiss(viewController: currentChild.viewControllable)
}
}
LoggedInInteractor에 의해서 cleanView 메소드가 호출되면, viewController를 닫음으로서 상위 RIB 뷰 계층구조에 하위 View를 남기지 않도록 보장한다.
LoggedIn RIB는 사용자가 OffGame 및 TicTacToe 사이를 전환할 수 있도록 허용해야 한다. 지금까지는 OffGame(=Start Game 스크린을 보여주는 역할)만 구현했으며, 사용자가 로그인을 하면 LoggedIn RIB에 제어권이 전달된다.
OffGame RIB의 Start Game 버튼을 누르면, TicTacToe RIB이 구현되고 전환되는 것을 구현하려고 한다.
이 과정은 LoggedIn RIB이 attach되고, LoggedOut RIB이 detach되는 과정과 유사하다.
1) TicTacToe로 전환하려면, LoggedInRouter 클래스에서 routeToTicTacToe 메소드를 구현해야한다.
2) OffGameViewController에서 OffGameInteractor로, 마지막으로 LoggedInInInteractor로 버튼 탭 이벤트를 와이어업해야 합니다.
게임이 끝나면, TicTacToe RIB에서 OffGame RIB으로 전환하려고 한다. 이미 TicTacToe RIB에는 listener가 세팅되어 있기 때문에, LoggedInRIB이 TicTacToe이벤트에 응답할 수 있도록 LoggedInInteractor만 구현하면 된다.
LoggedInRouting 프로토콜에 routeToOffGame 메소드를 선언한다.
protocol LoggedInRouting: Routing {
func routeToTicTacToe()
func routeToOffGame()
func cleanupViews()
}
LoggedInInteractor 클래스에 gameDidEnd 메소드를 구현한다.
// MARK: - TicTacToeListener
func gameDidEnd() {
router?.routeToOffGame()
}
LoggedInRouter에 routeToOffGame 메소드를 구현한다.
func routeToOffGame() {
detachCurrentChild()
attachOffGame()
}
private func detachCurrentChild() {
if let currentChild = currentChild {
detachChild(currentChild)
viewController.dismiss(viewController: currentChild.viewControllable)
}
}
TODO...