본문 바로가기

iOS

[iOS] RIBs - Tutorial 1

RIBs - Tutorial1

이전에 RIBs란 무엇인지 RIB은 어떻게 구성되는지 간단히 살펴보았습니다.

 

이제는 Uber에서 제공하는 Tutorial을 해보며 보다 자세히 살펴보도록 할게요.

 

이번 튜토리얼의 목표는 다양한 RIB에 대한 이해를 목표로 합니다.

 

또한 그들이 어떻게 상호작용 하며 통신을 하는지 살펴볼꺼에요!

 

프로젝트를 열고 아무것도 하지 않았으나 AppDelegate의 didFinishLaunchingWithOptions에서 에러가 발생합니다..ㅜㅜ

 

image

 

코드를 살펴보자니 앱의 main window 객체를 생성하고 RootBuilder 객체를 생성하여 launchRouter라는 변수에 초기화합니다.

 

그리고 launchRouter.laucnh(from:) 실행 부분에서 LaunchRouting 에는 launch라는 메소드가 존재하지 않다고 하네요??

 

LaunchRouter 파일 내부를 살펴보도록 할게요!

 

내부를 살펴보니 LaunchRouting이라는 프로토콜이 존재하며 해당 프로토콜을 채택하여 구현한 구현체 클래스 LaunchRouter가 존재합니다.

 

그리고 클래스 내부에 RootViewController와 keyWindow를 설정하는 코드가 적힌 메소드가 존재합니다.

 


public protocol LaunchRouting: ViewableRouting {

    /// Launches the router tree.
    func launchFromWindow(_ window: UIWindow)
}

public final func launchFromWindow(_ window: UIWindow) {
    window.rootViewController = viewControllable.uiviewController
    window.makeKeyAndVisible()

    interactable.activate()
    load()
}

 

AppDelegate에서 호출하고 있는 메소드와 파라미터도 동일한걸 보니 이름이 launch에서 launchFromWindow로 변경되었나 봐요!

 

해당 메소드를 호출해주도록 코드를 정정해주니 에러가 사라졌습니다 ㅎㅎ (편안 ㅎ)

 

자 이제 본격적으로 튜토리얼을 진행해보도록 할께요!

 

Project Structure


튜토리얼 1에 제공되는 boilerplate code 내부에는 2개의 RIBs가 포함되어 있습니다.

 

앱이 런칭하면 AppDelegate에서 RIB Tree의 최상단에 자리하게 되는 RootRIB을 빌드하게 됩니다.

 

두 번째 RIB은 LoggedOut이라는 RIB이며 해당 RIB은 인증 관련 로직을 실행하게 될 것입니다.

 

AppDelegate에 의해 Root RIB이 앱의 제어권을 얻게 되면 그 즉시 LoggedOut RIB을 띄워서 로그인 폼을 화면에 보여주도록 할 것입니다.

 

LoggedOut RIB은 아직 구현되어 있지 않으므로 튜토리얼을 진행하며 직접 만들어보도록 합시당.

 

Create LoggedOut RIB


Tooling에서 제공되는 스크립트를 실행하고 나면 Xcode 에서 새로운 파일을 생성할 때 RIB 단위 파일을 함께 생성할 수 있도록 해줍니다.

 

LoggedOut 디렉토리 내에 Owns corresponding view 옵션을 체크하여 RIB 파일을 생성해볼께요.

 

image

 

LoggedOut이라는 prefix를 갖는 Router, Interactor, Builder 그리고 View까지 생성되었습니다..ㅎ

 

LoggedOutViewController에서 제공되는 코드를 이용해 UI를 그려보도록 할께요.

 

import RIBs
import RxSwift
import UIKit
import SnapKit

protocol LoggedOutPresentableListener: class {
    // 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 = .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 = .line
        player1Field.layer.borderWidth = 2
        player1Field.layer.borderColor = UIColor.black.cgColor
        view.addSubview(player1Field)
        player1Field.placeholder = "Player 1 name"
        player1Field.snp.makeConstraints { make in
            make.top.equalTo(self.view).offset(100)
            make.leading.trailing.equalTo(self.view).inset(40)
            make.height.equalTo(40)
        }

        let player2Field = UITextField()
        self.player2Field = player2Field
        player2Field.borderStyle = .line
        player2Field.layer.borderWidth = 2
        player2Field.layer.borderColor = UIColor.black.cgColor
        view.addSubview(player2Field)
        player2Field.placeholder = "Player 2 name"
        player2Field.snp.makeConstraints { make in
            make.top.equalTo(player1Field.snp.bottom).offset(20)
            make.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 { make in
            make.top.equalTo(player2Field.snp.bottom).offset(20)
            make.left.right.height.equalTo(player1Field)
        }
        loginButton.setTitle("Login", for: .normal)
        loginButton.setTitleColor(.white, for: .normal)
        loginButton.backgroundColor = .black
        loginButton.addTarget(self, action: #selector(didTapLoginButton), for: .touchUpInside)
    }

    @objc
    private func didTapLoginButton() {

    }
}

 

LoggedOutViewController에 Snapkit을 이용하여 UI Layout을 잡고 Login 버튼 터치 시 작동하게 될 메소드도 임시로 생성하였습니다.

 

ViewController 파일 내에 Listener 프로토콜과 해당 프로토콜 객체를 가지는 ViewController 클래스가 존재합니다.

 

Listener Interface 내에 기재된 주석을 살펴보니 Listener 프로토콜 내에는 ViewController에서 발생 가능한 비즈니스 로직(ex: 로그인) 등이 위치하게 된다고 하네요~

 

그리고 이 비즈니스 로직은 Interactor의 역할이니 Interactor에서 해당 프로토콜을 채택하여 구현하게 되겠네요!

 

Login Logic


이전에 RIB 간 통신 방법에 대해 공부했던 내용을 한 번 리마인드 해볼께요!

 

parent to child 방향의 하향 통신의 경우에는 Rx stream을 전달하여 커뮤니케이션을 진행한다고 했고 child to parent 방향의 상향 통신의 경우에는 listener Interface를 통해 커뮤니케이션을 진행한다고 했었죠.

 

LoggedOut RIB은 RootRIB의 child RIB이며 LoggedOut RIB에서 발생한 로직 결과에 따라 앱의 상태가 변화하게 됩니다.

 

유저가 Login 버튼을 터치하게 되면 LoggedOutViewController는 본인의 Listener인 LoggedOutPresentableListener에게 해당 이벤트를 알립니다.

 

listener는 유저가 입력한 player의 name을 전달받아 추후 프로세스를 진행하게 됩니다.

 

해당 logic을 구현하기 위해서 우리는 listener가 ViewController로부터 login 요청을 받을 수 있도록 코드를 수정해주도록 합니다.

 

protocol LoggedOutPresentableListener: class {
    fucn login(withPlayer1Name player1Name: String?, player2Name: String?)
}

 

유저가 플레이어의 이름을 기입하지 않을 수도 있기 때문에 두 PlayerName 타입이 Optional 타입으로 정의되어있습니다.

 

우리는 두 names가 기입될 때 까지 Login 버튼을 disable 형태로 제어할 수 있으나 이번 튜토리얼에서는 LoggedOutInteractor 가 해당 예외 상황에 대해서 직접 처리하도록 할께요.

 

만일 playerName이 비어있다면 기본값을 제공하도록 구현해볼텐데 로그인 버튼이 터치되면 listener를 통해 메소드를 호출해야겠죠?

 

따라서 아래와 같이 didTapLoginButton() 메소드를 수정해줄께요.

 

@objc
private func didTapLoginButton() {
    listener?.login(withPlayer1Name: player1Field?.text, player2Name: player2Field?.text)
}

 

이제는 Listener Interface를 채택하여 구현하는 LoggedOutInteractor에서 마저 작업을 진행해줍니다.

 

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

 

name을 전달받아 empty 여부에 따라 기본값을 설정해주는 메소드를 정의하고 해당 메소드를 이용해 두 명의 플레이어 이름을 콘솔에 찍어주는 login이라는 메소드를 정의하였습니다.

 

이렇게 튜토리얼 1이 끝났습니다.

 

정리해보자면 AppDelegate에서 앱이 런칭한 이후 RIB 컴포넌트를 생성하고 DI를 정의하는 Builder를 통해 RootRIB을 생성하였습니다.

 

그리고 LoggedOut RIB을 처음부터 만들어 보았는데요, ViewController 에는 해당 ViewController에서 발생할 수 있는 비즈니스 로직에 대한 Listener Interface 프로토콜이 존재했으며 해당 프로토콜 객체를 가지고 있었습니다.

 

이 비즈니스 로직 수행은 Interactor의 역할이므로 Interactor가 해당 프로토콜을 채택하여 구현하였습니다.

 

특히 이 Listener라는 프로토콜을 통한 통신은 기존의 Delegate Pattern과 매우 유사하다는 느낌을 받았습니다.

 

다음 Tutorial에서 뵙도록 할께요!

 

Reference


'iOS' 카테고리의 다른 글

[iOS] [Swift] Enumerations  (0) 2021.06.05
[iOS] RIBs - Tutorial 2  (0) 2021.04.11
[iOS] RIBs?  (0) 2021.04.10
[iOS] [Swift] JSON Parsing with Codable  (0) 2021.02.27
[iOS] [Swift] POP - 프로토콜의 다형성  (0) 2021.01.01