RIBs - Tutorial2
안녕하세요 오늘은 이전에 진행했던 RIBs - Tutorial1에 이어 2편을 진행해보고자 합니다!
그럼 바로 시작하도록 할께요!
이전 튜토리얼에서 우리는 로그인 폼을 갖는 LoggedOut
RIB을 가지는 앱을 빌드해봤었는데요,
이번 튜토리얼에서는 거기서 더 나아가 로그인 이후 게임 필드를 보여주는 것 까지 진행해보도록 할께요.
이번 튜토리얼의 주요 목표는 아래와 같습니다.
- child RIB과 parent RIB의 커뮤니케이션.
- parent Interactor의 결정에 따른 child RIB Attach/Detach
- View를 가지지 않는 View-less 구조의 RIB 생성
Project Structure
이전 튜토리얼에서 우리는 Root RIB과 LoggedOut RIB 총 2개의 RIB을 완성했었죠.
이번에는 위 그림처럼 LoggedIn, OffGame 그리고 TicTacToe라는 이름을 가지는 3개의 RIB을 더 생성해보도록 할 것입니다.
여기서 LoggedIn RIB은 View를 갖지 않는 View-less RIB으로 생성할 거예요. 해당 RIB은 TicTacToe RIB과 OffGame RIB을 스위칭 해주기 위해 존재하기에 두 RIB의 공동 parent RIB으로 위치하게 됩니다.
OffGame RIB은 플레이어가 게임을 시작할 수 있도록 하는 기능을 제공하며 StartGame
이라는 버튼을 가지는 인터페이스를 제공할 거예요.
Communicating with a parent RIB
유저가 플레이어 이름을 기재한 이후 Login 버튼을 터치하게 되면, 우리는 Start Game
뷰를 보여줄 거예요.
해당 액션은 LoggedOutViewController에서 발생하게 되겠죠?
이전에 작성한 LoggedOut
RIB의 부모 RIB인 Root
RIB에게 이 이벤트를 알리고 RootRouter를 통해서 LoggedOut
RIB을 detach 하고 LoggedIn
RIB을 attach 해야겠죠.
앞서 얘기했듯 LoggedOut
RIB의 부모 RIB은 Root
RIB입니다. 따라서 LoggedOut
RIB에서 Root
RIB으로의 커뮤니케이션(상향 통신)을 할 때는 Listener Interface를 통하도록 할게요.
첫 번째로, LoggedOut 에서 Root에게 이벤트를 알릴 수 있도록 LoggedOutListener
프로토콜에 메서드를 아래와 같이 추가해볼께요!
// LoggedOutInteractor.swift
protocol LoggedOutListener: class {
func didLogin(withPlayer1Name player1Name: String, player2Name: String)
}
프로토콜에 메서드를 위와 같은 형태로 기재하게 되면 해당 프로토콜을 채택하는 구현체는 반드시 위 메서드를 구현하게 되죠.
따라서, LoggedOut RIB의 parent RIB은 반드시 didLogin
이라는 메서드를 구현하도록 강제됩니다.
이전에 Login 버튼이 터치되면 실행되도록 작성했던 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)
}
위와 같이 코드를 수정함으로써 우리는 유저가 Login 버튼을 터치하면 LoggedOut RIB의 listener에게 해당 Notification을 전달할 수 있게 되었습니다!
Routing to LoggedIn RIB
LoggedOut listener에게 Login 이벤트에 대한 Noti를 전달했다면 그 결과에 대한 액션을 처리해야겠죠?
이를 위해서 RootRouting 프로토콜을 아래와 같이 수정할꼐요.
protocol RootRouting: ViewableRouting {
func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String)
}
그러면 RootRouting 프로토콜을 채택하고 있는 RootInteractor 클래스에서 해당 메서드를 구현하도록 강제됩니다.
RootRouting 프로토콜 객체를 통해 해당 함수를 호출할 수 있도록 아래와 같이 구현해줍시다!
func didLogin(withPlayer1Name player1Name: String, player2Name: String) {
router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
}
위와 같이 코드를 작성해줌으로써 유저가 Login 버튼을 터치하면 Root RIB이 LoggedIn RIB으로 라우팅을 할 수 있도록 되었습니다.
이제 우리는 Routing을 통해 Attach 할 LoggedIn RIB을 작성해보도록 할께요!
Attaching a viewless LoggedIn RIB and detaching LoggedOut RIB when the user login
Root Router 가 새로 만든 RIB을 attach 하기 위해서는 RootRouter에서 해당 RIB을 구성하는 컴포넌트들을 생성하는 Builder를 가지고 있어야 합니다. 그래야 RIB을 만들 수 있고 RIB을 만들어야 attach 작업이 가능하기 때문이죠.
private 선언으로 loggedOutbuilder라는 상수를 하나 선언하고 RootRouter의 생성자에서 LoggedInBuildable
이라는 프로토콜을 전달받을 수 있도록 수정할께요!
final class RootRouter: LaunchRouter<RootInteractable, RootViewcontrollable>, RootRouting {
init(interactor: RootInteractable,
viewController: RootViewControllable,
LoggedOutBuilder: LoggedOutBuildable,
LoggedInBuilder: LoggedInBuildable) // LoggedInBuildable 추가
self.loggedOutBuilder = loggedOutBuilder
self.loggedInBuilder = loggedInBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
)
// MARK: - Private
private let loggedInBuilder: LoggedInBuidable
...
}
RootRouter 생성자에 파라미터가 추가되었으니 Router를 반환하는 Build 메서드에 에러가 발생합니다.
RootRIB을 생성하는 RootBuilder에서 LoggedInBuilder 객체를 생성하여 주입해줄 수 있도록 RootBuilder의 build 메서드를 수정하겠습니다.
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) // LoggedInBuilder 객체 생성
return RootRouter(interactor: interactor,
viewController: viewController,
loggedOutBuilder: loggedOutBuilder,
loggedInBuilder: loggedInBuilder) // 생성 시 loggedInBuilder 객체 전달
}
이제 RootRouter에서 LoggedIn RIB을 생성할 수 있도록 LoggedInBuilder를 갖게 되었어요!
그러면 RootRouter 에서 LoggedOut RIB을 detach 하고 LoggedIn RIB을 attach 하는 함수를 정의해보도록 할께요.
// 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)
}
위 함수에서 일어나는 로직이 정말 중요합니다.
Root RIB이 child RIB으로 LoggedOut RIB을 가지고 있는 상황이며 특정 유저의 액션에 따라 LoggedOut RIB을 parent RIB인 Root RIB으로부터 떼어내고 LoggedIn RIB을 생성하여 붙여줍니다.
RIBs 구조에서는 parent Router가 child 애 대한 attach/detach를 모두 제어합니다.
만일 attach/detach 작업 중 child RIB이 View를 가지고 있다면 반드시 present/dismiss 또한 함께 해주어야 합니다.
새로 생성한 LoggedIn RIB의 이벤트를 받기 위해서는 Root RIB의 Interacotr가 LoggedIn RIB의 Listener로 동작해야 하죠!
parent RIB의 Interactor 프로토콜인 RootInteractable 프로토콜이 child RIB의 Listener 프로토콜 LoggedInListener 프로토콜을 채택할 수 있도록 해줍니다.
protocol RootInteractable: Interactable, LoggedOutListener, LoggedInListener {
var router: RootRouting? { get set }
var listener: RootListener? { get set }
}
Attaching the OffGame RIB when the LoggedIn RIB Loads
우리가 방금 생성한 LoggedIn RIB은 View 컴포넌트를 가지지 않는 View-less RIB입니다.
즉, 비즈니스 로직을 통해 child RIB에 대한 제어만 가능하죠!
이번엔 Start Game이라는 버튼을 갖는 OffGame RIB을 생성해보도록 할께요.
RIB의 View 작업을 하기에 앞서 RootRouter가 LoggedInBuilder를 가지게 했던 것처럼 LoggedInRouter가 OffgameBuidler를 가지도록 LoggedInRouter의 생성자를 수정하겠습니다.
init(interactor: LoggedInInteractable,
viewController: LoggedInViewControllable,
offGameBuilder: OffGameBuildable) {
self.viewController = viewController
self.offGameBuilder = offGameBuilder // OffGameBuilder 객체 초기화
super.init(interactor: interactor)
interactor.router = self
}
)
private let offGameBuilder: OffGameBuildable
그리고 LoggedInRouter를 반환하는 build 메서드에서 OffGameBuilder 객체를 만들어서 전달해주도록 해요!
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)
}
이로써 LoggedInRouter가 child RIB인 OffGame RIB을 생성할 수 있게 되었습니다.
그러면 RIB을 생성해서 attach 해주는 메서드를 작성해볼께요!
private func attachOffGame() {
let offGame = offGameBuilder.build(withListener: interactor) // OffGameRouter 객체 생성
self.currentChild = offGame
attachChild(offGame)
viewController.present(viewController: offGame.viewControllable)
}
위 메서드를 통해 LoggedIn RIB의 child RIB으로 OffGame RIB이 attach 할 수 있게 되었어요!
그렇다면 child RIB에서 parent RIB으로의 통신을 위해 또 작업을 해줘야겠죠?
parent RIB의 Interactable 프로토콜이 child RIB의 Listener 프로토콜을 채택하도록 해줍니다!
protocol LoggedInInteractable: Interactable, OffGameListener {
weak var router: LoggedInRouting? { get set }
weak var listener: LoggedInListener? { get set }
}
이로써 OffGame RIB이 자식 RIB으로 붙은 이후에도 해당 RIB에서 발생하는 이벤트들을 부모 RIB이 listening 할 수 있는 상태가 되었습니다!
오늘은 RIBs - iOS Tutorial 2를 진행해봤는데요,
어떻게 RIB이 생성되는지, 생성된 RIB은 어떠한 방식에 의하여 attach 혹은 detach 되는지, attach 된 RIB은 부모 RIB과 어떻게 통신하는지에 대해 알 수 있었던 것 같습니다!
이론만 봤을 때는 뜬 구름을 잡는 것처럼 잘 이해가 되지는 않았는데 확실히 직접 Tutorial을 진행해보니 어느 정도 이해가 되는 것 같습니다 ㅎㅎ
다음 포스팅에서는 Tutorial 3 으로 찾아뵙도록 할께요!
Reference
'iOS' 카테고리의 다른 글
[iOS] [Swift] The Swift Language Guide - 1. 기본 연산자 (Basic Operators) (0) | 2021.06.22 |
---|---|
[iOS] [Swift] Enumerations (0) | 2021.06.05 |
[iOS] RIBs - Tutorial 1 (1) | 2021.04.10 |
[iOS] RIBs? (0) | 2021.04.10 |
[iOS] [Swift] JSON Parsing with Codable (0) | 2021.02.27 |