iOS

RIBs) tutorial1

삼쓰 웅쓰 2021. 6. 25. 23:58
728x90
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
    }
}

 

실행화면