RIBs) tutorial1
uber 공식 튜토리얼을 따라가면서 제 나름대로 정리한 내용입니다.
보다 정확한 내용은 원문을 참고해주시고 오류나 수정사항이 있으면 알려주시면 감사하겠습니다. 🙇🏻♂️
전체 코드를 보시려면 여기로 ;)
목표
간단한 이 튜토리얼들은 간단한 tictactoe 게임을 만들어보는 것입니다.
튜토리얼을 따라가보면서 RIB 에 대해 이해하고 서로가 어떻게 상호작용하는지 알아봅니다 :)
Project 구조
먼저 튜토리얼 템플릿을 다운받습니다.
기본 템플릿에는 Root, LoggedOut 2개의 RIB 폴더가 만들어져 있네요.
제일 먼저 AppDelegate 에서 RIB을 생성해주면서 시작되는데요, Root RIB은 이미 만들어져 있고, LoggedOut은 DELETE_ME 라는 파일만 있습니다. 원하는 대로 DELETE_ME를 제거하고 LoggedOut RIB을 만들어 봅시다.
LoggedOut RIB 생성
기본 템플릿에는 이미 tooling이 설치되어 있어서 새 파일 만들기(cmd+N)를 보면 RIB 파일을 확인할 수 있으실거에요. 눌러서 LoggedOut RIB을 만들어줍니다.
RIB은 뷰를 가질 수도 있고 가지고 있지 않을 수도 있는데요,
저렇게 Owns corresponding view 를 체크해서 생성하면 View (ViewController) 를 가진 RIB 파일들이 생성됩니다.
생성된 코드 이해하기
무슨 파일들이 만들어졌는지 살펴봅시다.
RIB은 Router, Interactor, Builder를 뜻합니다.
- LoggedOutBuilder: LoggedOutBuildable 프로토콜을 채택해서 각 요소들을 만듭니다.
// MARK: - Builder
protocol LoggedOutBuildable: Buildable {
func build(withListener listener: LoggedOutListener) -> LoggedOutRouting
}
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: Interactor는 RIBs의 핵심입니다. 위 그림에서 볼 수 있듯이 다른 RIB으로 전환시키기도 하고 뷰와 상호작용 합니다. 따라서 이를 위한 객체들을 가지고 있습니다. 이때 구현된 객체를 대신 프로토콜을 사용해 의존성을 역전시켜줍니다.
- LoggedOutRouter: 앞선 LoggedOutRouting을 채택해서 구현된 구현체입니다. 실제로 구체적인 Route 로직을 작성하는 부분인데요, interactor와 viewController를 주입받아서 통신할 수 있도록 합니다.
- LoggedOutViewController: RIB을 생성할 때 Owns corresponding view를 체크해줬기 때문에 자동으로 생성된 뷰컨트롤러 입니다. listener를 통해서 사용자 이벤트를 처리합니다. 이때도 역시 프로토콜을 사용해 의존성을 역전시켜줍니다
protocol LoggedOutPresentableListener: AnyObject {
}
final class LoggedOutViewController: UIViewController, LoggedOutPresentable, LoggedOutViewControllable {
weak var listener: LoggedOutPresentableListener?
}
LoggedOut UI
로그인이 안 되어있는 상태니 간단한 로그인 화면이 필요합니다. 여기선 뷰가 중요한 게 아니니 우버에서 친절히 만들어주신 코드를 고대로 복붙 하고 넘어가죠 ;)
아래 코드로 LoggedOutViewController를 채워줍니다.코드보기
import RIBs
import RxSwift
import UIKit
import SnapKit
protocol LoggedOutPresentableListener: AnyObject {
// 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 LoggedOutViewController: UIViewController, LoggedOutPresentable, LoggedOutViewControllable {
weak var listener: LoggedOutPresentableListener?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
let playerFields = buildPlayerFields()
buildLoginButton(withPlayer1Field: playerFields.player1Field, player2Field: playerFields.player2Field)
}
// MARK: - Private
private var player1Field: UITextField?
private var player2Field: UITextField?
private func buildPlayerFields() -> (player1Field: UITextField, player2Field: UITextField) {
let player1Field = UITextField()
self.player1Field = player1Field
player1Field.borderStyle = UITextBorderStyle.line
view.addSubview(player1Field)
player1Field.placeholder = "Player 1 name"
player1Field.snp.makeConstraints { (maker: ConstraintMaker) in
maker.top.equalTo(self.view).offset(100)
maker.leading.trailing.equalTo(self.view).inset(40)
maker.height.equalTo(40)
}
let player2Field = UITextField()
self.player2Field = player2Field
player2Field.borderStyle = UITextBorderStyle.line
view.addSubview(player2Field)
player2Field.placeholder = "Player 2 name"
player2Field.snp.makeConstraints { (maker: ConstraintMaker) in
maker.top.equalTo(player1Field.snp.bottom).offset(20)
maker.left.right.height.equalTo(player1Field)
}
return (player1Field, player2Field)
}
private func buildLoginButton(withPlayer1Field player1Field: UITextField, player2Field: UITextField) {
let loginButton = UIButton()
view.addSubview(loginButton)
loginButton.snp.makeConstraints { (maker: ConstraintMaker) in
maker.top.equalTo(player2Field.snp.bottom).offset(20)
maker.left.right.height.equalTo(player1Field)
}
loginButton.setTitle("Login", for: .normal)
loginButton.setTitleColor(UIColor.white, for: .normal)
loginButton.backgroundColor = UIColor.black
loginButton.addTarget(self, action: #selector(didTapLoginButton), for: .touchUpInside)
}
@objc private func didTapLoginButton() {
}
}
LoggedOut logic
이제 login 하는 로직을 작성해볼 차례입니다!
위에서 잠깐 말씀드렸듯이 뷰에서는 리스너를 사용해 사용자 이벤트를 처리합니다.
지금 상황에서는 Login인 버튼이 눌렸을 때 사용자 이름을 넘겨서 로그인하는 함수가 필요합니다. 뷰컨에서 위에 있는 리스너 프로토콜에 아래와 같이 메소드를 정의해줍니다.
뷰에서는 리스너에 정의한 메소드를 호출하는 것까지가 자신의 할 일입니다. 뷰에는 리스너 프로토콜을 객체로 들고 호출해줍니다.
protocol LoggedOutPresentableListener: class {
func login(withPlayer1Name player1Name: String?, player2Name: String?)
}
// ... in LoggedOutViewController
weak var listener: LoggedOutPresentableListener?
@objc private func didTapLoginButton() {
listener?.login(withPlayer1Name: player1Field?.text, player2Name: player2Field?.text)
}
실제 로직은 Interactor에서 처리하게 됩니다. LoggedOutInteractor에서 뷰의 리스너인 LoggedOutPresentableListener 을 채택하고(이미 되어있음), 아래 코드를 구현합니다.
여기선 player들의 이름이 잘 프린트 되는지까지만 확인해보고 2탄에서 만나요 🙋🏼♂️
// 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
}
}