iOS

[iOS] RIBs 튜토리얼3

Kim_Baechu 2022. 3. 12. 02:29

https://github.com/uber/RIBs/wiki/iOS-Tutorial-3

RIBs Dependency Injection and Communication

source code here

Follow the README to install and open

Goals

  • Builder의 빌드 메서드를 통해 동적 종속성을 자식 RIB로 전달합니다.
  • DI(종속성 주입 트리)를 사용하여 정적 종속성을 전달합니다.
  • Swift의 확장 기반 종속성 준수.
  • RIB 라이프사이클을 사용하여 Rx 스트림 라이프사이클 관리.

Dynamic dependencies

이 튜토리얼에서는 선수 이름을 OffGame과 TicTacToe RIBs로 RIB 트리에 전달합니다.

우리는 플레이어 이름을 LoggedInBuilder의 빌드 메서드를 통해 Root RIB에서 LoggedIn RIB로 동적 의존성을 전달하는 것으로 시작하겠습니다.

이를 위해 LoggedInBuildable 프로토콜을 업데이트하여 기존 listener 의존성 외에 두 플레이어 이름을 동적 의존성으로 포함합니다.

protocol LoggedInBuildable: Buildable {
    func build(withListener listener: LoggedInListener,
               player1Name: String,
               player2Name: String) -> LoggedInRouting
}

Then we'll update the implementation of the LoggedInBuilder's build method:

func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -> LoggedInRouting {
    let component = LoggedInComponent(dependency: dependency,
                                      player1Name: player1Name,
                                      player2Name: player2Name)

DI tree에 플레이어 이름을 넣기 위해 LoggedInComponent 생성자를 업데이트합니다.

We'll store them as constants in the LoggedInComponent:

let player1Name: String
let player2Name: String

init(dependency: LoggedInDependency, player1Name: String, player2Name: String) {
    self.player1Name = player1Name
    self.player2Name = player2Name
    super.init(dependency: dependency)
}

This effectively transforms the player names from dynamic dependencies provided by the LoggedIn’s parent to the static dependencies available to any of the LoggedIn’s children.

이렇게 하면 LoggedIn의 부모가 제공하는 동적 의존성으로부터 플레이어 이름을

LoggedIn’s 자식에서 사용가능한 스태틱 의존성으로 효과적으로 변환할 수 있습니다.

Next, we'll update the RootRouter class to pass in the player names to the LoggedInBuildable’s build method:

func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) {
    // Detach logged out.
    if let loggedOut = self.loggedOut {
        detachChild(loggedOut)
        viewController.dismiss(viewController: loggedOut.viewControllable)
        self.loggedOut = nil
    }

    let loggedIn = loggedInBuilder.build(withListener: interactor, player1Name: player1Name, player2Name: player2Name)
    attachChild(loggedIn)
}

유저로 부터 입력받고, LoggedOut RIB으로부터 처리된 유저이름이 LoggedIn RIB과 그 자식들에서 사용가능해집니다.

Dynamic dependencies vs static dependencies

보시다시피 LoggedInRIB를 빌드할 때 플레이어 이름을 RIB에 동적으로 삽입하기로 결정했습니다.

대신 LoggedInRIB 트리 아래로 전달하여 이러한 의존성을 정적으로 해결하도록 RIB를 구성할 수 있습니다.

그러나 이 경우 Root RIB가 생성될 때 플레이어 이름을 초기화할 수 없기 때문에 optional로 만들어야 합니다.(여기서 기본값으로 초기화하는 것이 여기에서 옵션이 아니라고 가정하겠습니다).

optional 값을 처리하기로 결정했다면 RIB 코드에 추가적인 복잡성을 도입했을 것입니다.

LoggedInRIB과 자식들은 nil 값을 적절한 선수 이름으로 대신 처리해야 합니다.and doing so would clearly be beyond their responsibilities.

대신 사용자가 이름을 입력한 후(또는 nil처리 후) 최대한 빨리 플레이어 이름을 처리하고 앱의 다른 부분에서 이러한 부담을 제거해야 합니다.

적절한 범위의 의존성을 통해 불변 가정을 할 수 있으므로 불합리하거나 불안정한 코드를 제거할 수 있습니다.

RIB's Dependencies and Components

RIB 용어에서 Dependency은 RIB가 적절하게 인스턴스화하기 위해 부모로부터 필요한 의존성을 나열하는 프로토콜입니다.

Component는 Dependency 프로토콜의 구현입니다.

RIB의 빌더에 부모 의존성을 제공하는 것 외에도 Component는 RIB가 자체 및 자식에 대해 생성하는 의존성을 소유할 책임이 있습니다.

일반적으로 부모 RIB가 자식 RIB를 인스턴스화할 때 자신의 component를 생성자 의존성으로 자식 빌더에 삽입합니다. 주입된 각 component는 자체적으로 자식에게 노출할 의존성을 결정합니다.

Component에 포함된 종속성은 일반적으로 DI 트리 아래로 전달되어야 하는 일부 상태를 보유하거나 구성하는 데 비용이 많이 들고 성능상의 이유로 RIB 간에 공유됩니다.

**Passing the player names to the OffGame scope using the DI tree

이제 플레이어 이름을 확인하고 LoggedInRIB에 삽입할 때 유효한지 확인했으므로 DI 트리를 RIB로 안전하게  OffGame RIB에 전달하여 "Game Start" 버튼 옆에 표시할 수 있습니다.

For this, we'll declare the player names as dependencies in the OffGameDependency protocol:

protocol OffGameDependency: Dependency {
    var player1Name: String { get }
    var player2Name: String { get }
}

OffGame이러한 정적 종속성은 부모 RIB에 의해 초기화되는 동안 OffGame RIB에 전달되어야 합니다 .

다음 단계로 OffGameComponent에 정의된 구성 요소 클래스를 사용하여 OffGame의 자체 범위 에서 이러한 종속성을 사용할 수 있도록 합니다

final class OffGameComponent: Component<OffGameDependency> {
    fileprivate var player1Name: String {
        return dependency.player1Name
    }

    fileprivate var player2Name: String {
        return dependency.player2Name
    }
}

프로퍼티가  fileprivate 임을 확인하세요.

이는 OffGameBuilder.swift파일 내에서만 액세스할 수 있으므로 자식 범위에 노출되지 않음을 의미합니다.

우리는 이러한 값을 OffGame자식 범위에 제공하기를 원했기 때문에 fileprivate액세스 제어를 LoggedInComponentOffGame 에 사용하지 않았습니다 .

이전 단계에서 이미 플레이어 이름을 LoggedInComponent에 추가 했으므로 ,방금 추가한 새 의존성을 충족하기 위해 OffGame의 부모 범위(LoggedIn 범위)를 만들기 위해 해야 할 일은 없습니다 .

다음으로 이러한 의존성을 OffGameViewController생성자 주입을 통해 전달하여 표시합니다.

또한 의존성을 OffGameInteractor첫 번째 항목에 전달하고 인터랙터가 이 정보를 표시하기 위해 OffGamePresentable 메서드를 호출하도록 할 수도 있지만 플레이어 이름을 표시하는 데 추가 처리가 필요하지 않기 때문에 표시할 뷰 컨트롤러에 직접 전달할 수 있습니다.

OffGameViewController에서 생성자를 통해 전달된 값을 저장하기 위해 player1Name및 player2Name상수를 사용 합니다.

Let's update the OffGameBuilder to inject the dependencies into the view controller.

final class OffGameBuilder: Builder<OffGameDependency>, OffGameBuildable {
    override init(dependency: OffGameDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: OffGameListener) -> OffGameRouting {
        let component = OffGameComponent(dependency: dependency)
        let viewController = OffGameViewController(player1Name: component.player1Name,
                                                   player2Name: component.player2Name)
        let interactor = OffGameInteractor(presenter: viewController)
        interactor.listener = listener
        return OffGameRouter(interactor: interactor, viewController: viewController)
    }
}

And modify the OffGameViewController to store the player names received during initialization.

...

private let player1Name: String
private let player2Name: String

init(player1Name: String, player2Name: String) {
    self.player1Name = player1Name
    self.player2Name = player2Name
    super.init(nibName: nil, bundle: nil)
}

...

Finally, we will have to update the UI of the OffGame RIB's view controller to display the player names on screen. To save time, you may use the provided code here.

Track scores using a ReactiveX stream

현재 응용 프로그램은 게임 후 점수를 추적하지 않습니다.

게임이 끝나면 사용자는 단순히 시작 화면으로 리디렉션됩니다.

앱을 개선하여 점수를 업데이트하고 시작 화면에 표시하도록 하겠습니다.

그렇게 하기 위해 우리는 reactive 스트림을 만들고 observe할 것입니다.

반응형 프로그래밍 기술은 RIB 아키텍처에서 널리 사용됩니다.

가장 일반적인 용도 중 하나는 RIB 간의 통신을 용이하게 하는 것입니다.

자식 RIB가 부모로부터 동적 데이터를 수신해야 하는 경우 생산자 측에서 관찰 가능한 스트림으로 데이터를 래핑하고 소비자 측에서 이 스트림을 구독하는 것이 일반적입니다.

이 튜토리얼은 독자가 반응 프로그래밍과 관찰 가능한 스트림의 주요 개념에 익숙하다고 가정합니다.

그들에 대해 더 알고 싶다면 ReactiveX 문서 를 참조하십시오 .

우리의 경우 이 TicTacToeRIB가 현재 게임의 상태를 제어하므로 게임 점수는 RIB에 의해 업데이트되어야 합니다 .

이 점수는 이 OffGameRIB가 소유한 화면에 표시되므로 RIB에서 읽어야 합니다 .

TicTacToe와OffGame 는 서로 알지 못하고 직접 데이터를 교환할 수 없습니다.

그러나 둘 다 동일한 부모인 LoggedInRIB가 있습니다.

두 자식 모두에게 스트림에 대한 액세스 권한을 부여하려면 이 RIB에서 점수 스트림을 구현해야 합니다.

LoggedIn 그룹에 ScoreStream 를 만듭니다.

미리 구현된 소스 → here.

두개의 점수 stream protocol,하나는 읽기전용 스트림 ScoreStream, 다른 하나는 변경가능한 버전 MutableScoreStream.

Create a shared ScoreStream instance in LoggedInComponent.

var mutableScoreStream: MutableScoreStream {
    return shared { ScoreStreamImpl() }
}

A shared instance means a singleton created for the given scope (in our case, the scope includes the LoggedIn RIB and all its children). The streams are typically scoped singletons, as with most stateful objects. The majority of other dependencies should however be stateless, and therefore not shared.

공유 인스턴스는 지정된 범위에 대해 생성된 싱글톤을 의미합니다(이 경우 범위에는 LoggedInRIB 및 모든 자식 항목이 포함됨).

스트림은 대부분의 상태 저장 개체와 마찬가지로 일반적으로 범위가 지정된 싱글톤입니다.

그러나 대부분의 다른 종속성은 상태가 없어야 하므로 공유되지 않아야 합니다.

mutableScoreStream프로퍼티는  fileprivate 가 아니라 (암시적) internal액세스 수정자를 사용하여 생성되었습니다.

이 속성은 LoggedIn의 자식 이 액세스할 수 있어야 하므로 파일 외부에 노출해야 합니다 .

이 요구 사항이 충족되지 않으면 선언 파일 내에서 스트림을 캡슐화하는 것이 좋습니다.

Furthermore, only those dependencies that are directly used in the RIB should be placed in the base implementation of the component, with the exception being stored properties that are injected from dynamic dependencies, such as the player names. In this case, because LoggedIn RIB will directly use the mutableScoreStream in the LoggedInInteractor class, it is appropriate for us to place the stream in the base implementation. Otherwise, we would have placed the dependency in the extension, e.g. LoggedInComponent+OffGame.

또한 RIB에서 직접 사용되는 의존성만 component의 기본 구현에 배치해야 합니다.

단, 플레이어 이름과 같은 동적 의존성에서 주입되는 저장된 속성은 예외입니다.

이 경우 LoggedInRIB는 LoggedInInteractor클래스 에서 mutableScoreStream 를 직접 사용하므로 기본 구현에 스트림을 배치하는 것이 적절합니다.

그렇지 않으면 extension에 의존성을 배치했을 것입니다(예: LoggedInComponent+OffGame)

Now, let's pass the mutableScoreStream into the LoggedInInteractor so that it could update the scores later. We’ll also need to update the LoggedInBuilder to make the project compile.

...

private let mutableScoreStream: MutableScoreStream

init(mutableScoreStream: MutableScoreStream) {
    self.mutableScoreStream = mutableScoreStream
}

...
func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -> LoggedInRouting {
    let component = LoggedInComponent(dependency: dependency,
                                      player1Name: player1Name,
                                      player2Name: player2Name)
    let interactor = LoggedInInteractor(mutableScoreStream: component.mutableScoreStream)

...

Passing a read-only ScoreStream down to OffGame scope for displaying

플레이어 이름을 결정하고 LoggedIn RIB에 주입할 때 유효한지 확인했으므로 DI 트리를 OffGame RIB로 안전하게 전달하여 "게임 시작" 단추와 함께 표시할 수 있습니다.

For this, we'll declare the player names as dependencies in the OffGameDependency  protocol:

protocol OffGameDependency: Dependency {
    var player1Name: String { get }
    var player2Name: String { get }
    var scoreStream: ScoreStream { get }
}

Then we'll provide the dependency to the current scope in OffGameComponent:

fileprivate var scoreStream: ScoreStream {
    return dependency.scoreStream
}

The OffGame's builder should be modified to inject the stream into the OffGameInteractor for later use.

func build(withListener listener: OffGameListener) -> OffGameRouting {
    let component = OffGameComponent(dependency: dependency)
    let viewController = OffGameViewController(player1Name: component.player1Name,
                                               player2Name: component.player2Name)
    let interactor = OffGameInteractor(presenter: viewController,
                                       scoreStream: component.scoreStream)

Finally, we should update the OffGameInteractor constructor to receive the score stream and store it in a private constant:

...

private let scoreStream: ScoreStream

init(presenter: OffGamePresentable,
     scoreStream: ScoreStream) {
    self.scoreStream = scoreStream
    super.init(presenter: presenter)
    presenter.listener = self
}

...

OffGame's component에서 점수 스트림 변수를 정의할 때 LoggedIn RIB의 정의와는 달리 fileprivate 액세스 한정자를 사용합니다.

왜냐하면 우리는 이 의존성을 현재 OffGame’s 자식에게 노출시킬 의도가 없기 때문입니다.

읽기 전용 점수 스트림은 OffGame 범위에서만 필요하며 LoggedIn RIB에서는 사용되지 않으므로 LoggedInComponent+OffGame extension에 의존성을 배치합니다.

이 파일의 stub 구현이 이미 제공되었습니다.

제공된 확장 스텁으로 작업하거나 파일을 삭제하고 구성 요소 확장 Xcode 템플릿을 사용하여 파일을 다시 생성할 수 있습니다.

구성 요소 확장 작업 방법에 대해 더 잘 이해하려면 파일의 TODO 주석을 읽어보십시오.

Add the score stream as a dependency to the component extension.

extension LoggedInComponent: OffGameDependency {
    var scoreStream: ScoreStream {
        return mutableScoreStream
    }
}

MutableScoreStream 프로토콜은 읽기 전용 버전을 확장하기 때문에 읽기 전용 스트림 구현을 기대하는 자식에게 이 버전을 노출시킬 수 있습니다.

Display the scores by subscribing to the score stream

이제 OffGame RIB는 스코어 스트림을 구독해야 합니다.

스트림이 내보내는 새로운 Score 값에 대해 알림을 받은 후 OffGamePresentable은 이 값을 뷰 컨트롤러로 전달하여 화면에 표시합니다.

반응형 구독을 사용하여 저장된 상태를 제거하고 데이터 변경 사항을 반영하여 UI를 자동으로 업데이트합니다.

점수 값을 설정할 수 있도록 OffGamePresentable 프로토콜을 업데이트합시다.

기억하세요, 이것은 인터랙터에서 뷰로 통신하기 위해 사용하는 프로토콜입니다

protocol OffGamePresentable: Presentable {
    weak var listener: OffGamePresentableListener? { get set }
    func set(score: Score)
}

We create a subscription in the OffGameInteractor class and invoke the OffGamePresentable to set the new score when the stream emits a value.

OffGameInteractor 클래스에서 구독을 만들고 OffGamePresentable을 호출하여 스트림이 값을 방출할 때 새 점수를 설정합니다.

private func updateScore() {
    scoreStream.score
        .subscribe(
            onNext: { (score: Score) in
                self.presenter.set(score: score)
            }
        )
        .disposeOnDeactivate(interactor: self)
}

Here we use the disposeOnDeactivate extension to handle our Rx subscription’s lifecycle. As the name suggests, the subscription is automatically disposed when the given interactor, in this case, the OffGameInteractor, is deactivated. We should almost always create Rx subscriptions in our interactor or worker classes to take advantage of these Rx lifecycle management utilities.

We then invoke the updateScore method in OffGameInteractor’s didBecomeActive lifecycle method. This allows us to create a new subscription whenever the OffGameInteractor is activated, which ties nicely with the use of disposeOnDeactivate.

disposeOnDeactivate extension을 사용하여 Rx 구독의 생명 주기를 처리합니다.

이름에서 알 수 있듯이, 주어진 인터랙터, 이 경우 OffGameInteractor가 비활성화되면 가입이 자동으로 dispose됩니다.

우리는 이러한 Rx 라이프사이클 관리 유틸리티를 이용하기 위해 Rx를 사용합니다.

그런 다음 OffGameInteractor의 didBecomeActive 라이프사이클 메서드에서 'updateScore' 메서드를 호출합니다.

이를 통해 OffGameInteractor 가 활성화될 때마다 새로운 구독을 만들 수 있는데, 이는 disposeOnDeactivate의 사용과 잘 맞아떨어진다.

override func didBecomeActive() {
    super.didBecomeActive()

    updateScore()
}

마지막으로 UI를 구현하여 점수를 표시해야 합니다.

OffGameViewControllerimplementation 구현 → here

지금 앱을 빌드하고 시작하면 시작 화면에 점수가 표시됩니다.

점수가 변경될 때마다 OffGame RIB에 알리는 리액티브 구독을 추가하고 뷰 컨트롤러를 업데이트하여 변화에 대응했습니다.

하지만 우리는 아직 게임 종료 후 스코어를 실제로 갱신할 수 있는 코드를 가지고 있지 않습니다.

Updating the score stream when a game is over

After a game is over, the TicTacToe RIB invokes its listener to call up to LoggedInInteractor. This is where we should update our score stream.

Update TicTacToe’s listener to share the information about the game winner.

protocol TicTacToeListener: class {
    func gameDidEnd(withWinner winner: PlayerType?)
}

이제 방금 업데이트한 리스너에 우승자를 전달하기 위해 TicTacToeInteractor 구현을 업데이트하겠습니다.

이렇게 하는 데는 여러 가지 방법이 있습니다.

승자를 TicTacToeInteractor의 지역 변수에 저장하거나 사용자가 alert를 닫은 후 CloseGame 메서드로 승자를 TicTacToeViewController 가 다시 인터랙터로 전달할 수 있습니다.

엄밀히 말하면, 두 가지 방법 모두 올바르고 적절합니다.

두 솔루션의 장점과 단점에 대해 살펴보겠습니다.

TicTacToeInteractor에 저장된 로컬 변수를 사용하면 인터랙터 내에 필요한 모든 데이터를 캡슐화한다는 이점이 있습니다.

단점은 우리가 지역적이고 mutable 상태를 유지해야 한다는 것입니다.

이것은 우리의 RIB이 범위가 넓다는 사실에 의해 어느 정도 완화됩니다.

각 RIB의 국소 상태는 잘 캡슐화되어 있고 제한적입니다.

새로운 게임을 런칭한 후 새로운 TicTacToe RIB를 만들면 이전 것은 모든 지역 변수와 함께 할당이 해제됩니다.

뷰 컨트롤러에서 데이터를 다시 전달하는 방법을 사용할 경우 인터랙터에 로컬 변경 가능 상태를 저장하지 않아도 되지만 비즈니스 논리를 처리할 때는 뷰 컨트롤러에 의존해야 합니다.

두 장점을 모두 이용하려면 스위프트 클로저를 이용해야 합니다.

인터랙터가 뷰 컨트롤러에게 게임이 끝났음을 알릴 때 뷰 컨트롤러가 상태를 업데이트한 후 호출되는 completion handler를 제공할 수 있습니다.

이렇게 하면 승자가 TicTacToeInteractor 내에 캡슐화되어 추가 상태가 저장되지 않습니다.

또한 뷰 컨트롤러의 리스너에서 closeGame 메소드를 불필요하게 만듭니다.

Here's how we can use a completion handler to update the score.

In TicTacToePresentableListener, remove the declaration of closeGame method.

protocol TicTacToePresentableListener: class {
    func placeCurrentPlayerMark(atRow row: Int, col: Int)
}

In TicTacToeViewController, modify the announce method to receive the completion handler as an argument and invoke it after the user dismisses an alert.

TicTacToeViewController에서 completion handler를 인수로 수신하도록 announce 메서드를 수정하고 사용자가 alert을 해제한 후 호출합니다.

announce 메서드는 인터랙터가 플레이어 중 한 명이 게임에서 이겼다고 판단한 후 인터랙터에 의해 호출됩니다.

func announce(winner: PlayerType?, withCompletionHandler handler: @escaping () -> ()) {
    let winnerString: String = {
        if let winner = winner {
            switch winner {
            case .player1:
                return "Red won!"case .player2:
                return "Blue won!"
            }
        } else {
            return "It's a draw!"
        }
    }()
    let alert = UIAlertController(title: winnerString, message: nil, preferredStyle: .alert)
    let closeAction = UIAlertAction(title: "Close Game", style: UIAlertActionStyle.default) { _ in
        handler()
    }
    alert.addAction(closeAction)
    present(alert, animated: true, completion: nil)
}

In TicTacToePresentable protocol, update the declaration of announce method to include the completion handler argument.

protocol TicTacToePresentable: Presentable {
    ...

    func announce(winner: PlayerType?, withCompletionHandler handler: @escaping () -> ())
}

In TicTacToeInteractor, update the call to announce method of the presenter with a completion handler.

func placeCurrentPlayerMark(atRow row: Int, col: Int) {
    guard board[row][col] == nil else {
        return
    }

    let currentPlayer = getAndFlipCurrentPlayer()
    board[row][col] = currentPlayer
    presenter.setCell(atRow: row, col: col, withPlayerType: currentPlayer)

    if let winner = checkWinner() {
        presenter.announce(winner: winner) {
            self.listener?.gameDidEnd(withWinner: winner)
        }
    }
}

Finally, in LoggedInInteractor update the implementation of gameDidEnd method to update the score stream when there's a new winner.

func gameDidEnd(withWinner winner: PlayerType?) {
    if let winner = winner {
        mutableScoreStream.updateScore(withWinner: winner)
    }
    router?.routeToOffGame()
}

이제 점수 스트림 완전히 기능적입니다.

경기를 이길 때마다 점수가 정확하게 업데이트되 보일 것입니다.

Bonus exercises

There are two more things we recommend you to do to improve the app and better understand the communication between the RIBs.

The first thing would be improving an alert shown after the game is over. At the moment, instead of showing a name of the winning player we use hardcoded names "Red" and "Blue". You could pass down the player names from the LoggedIn scope to the TicTacToe scope and display them in the alert instead.

Another nice improvement would be dealing with the draws. At the moment, the game gets stuck when it ends in a draw with all game fields marked. You can update the game logic, the user interface, and score calculation to handle this case.