ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RIBs) tutorial2-2
    iOS 2021. 6. 26. 14:12
    uber 공식 튜토리얼을 따라가면서 제 나름대로 정리한 내용입니다. 
    보다 정확한 내용은 원문을 참고해주시고 오류나 수정사항이 있으면 알려주시면 감사하겠습니다. 🙇🏻‍♂️
    전체 코드를 보시려면 여기로 ;)

     

    ⚠️ 우버 튜토리얼의 순서를 그대로 따라가지 않습니다. 
    이전 튜토리얼에서 부터 만들어나갑니다.

     

     

    TicTacToe

    이제 진짜 게임을 시작할 차례입니다.

    앞선 LoggedOut → LoggedIn 과 비슷합니다.

    이번엔 OffGame → TicTacToe로
    OffGame 에서 Start Game 버튼이 눌리면 TicTacToe RIB으로 이동시켜줍니다.
    똑~~같습니다.

    먼저 TicTacToe RIB을 만들어줍니다. 당연히 뷰가 있는 RIB 이겠죠?

    원래 튜토리얼에는 틱택토 게임이 구현된 TicTacToe RIB이 이미 만들어져 있는데요. 저희도 역시 거기서 뷰컨과 인터렉터를 고대로 가져오겠습니다.

     

    이제 TicTacToe RIB도 준비가 되었으니 저희가 해야할 건 Start Game 버튼이 눌리면 OffGame RIB을 제거하고 TicTacToe RIB을 붙이는 일입니다.

    먼저 Start Game 버튼이 눌렸을 때를 처리해보겠습니다. PresentableListener에 메소드를 만들고 버튼에 셀렉터를 달아서 호출합니다.

    protocol OffGamePresentableListener: class {
        func startGame()
    }
    
    // in OffGameViewContriller
    
    ...
    
    private func buildStartButton() {
        ...
        startButton.addTarget(self, action: #selector(didTapStartButton), for: .touchUpInside)
    }
    
    @objc private func didTapStartButton() {
        listener?.startGame()
    }

     

    PresentableListener가 수정되었으니 자연스럽게 Interactor도 수정해줘야 합니다.

    protocol OffGameListener: AnyObject {
        func didStartGame()
    }
    
    // ... in OffGameInteractor
    
    
    // MARK: - OffGamePresentableListener
    
    func startGame() {
        listener?.didStartGame()
    }

     

    OffGameListener는 그 부모인 LoggedInInteractable가 채택하고 있게 처리해줬었죠?
    또 자연스럽게 LoggedInInteractable에 didStartGame 게임을 구현해주겠습니다.
    여기선 TicTacToe로 route 시켜줘야겠네요.

    protocol LoggedInRouting: Routing {
        func cleanupViews()
        func routeToTicTacToe()
    }
    
    // ... in LoggedInInteractor
    
    func didStartGame() {
        router?.routeToTicTacToe()
    }

     

    역시 자연스럽게 LoggedInRouting이 수정되었으니 이걸 채택하는 LoggedInRouter로 가서 구현해줘야 하는데요,
    LoggedInRouter에서 TicTacToe를 붙이려면 만들 수 있어야 하니 TicTacToe Builder를 먼저 만들어줍니다.

    // ... in LoggedInRouter
    
    init(interactor: LoggedInInteractable,
         viewController: LoggedInViewControllable,
         offGameBuilder: OffGameBuildable,
         ticTacToeBuilder: TicTacToeBuildable) {
        self.viewController = viewController
        self.offGameBuilder = offGameBuilder
        self.ticTacToeBuilder = ticTacToeBuilder
        super.init(interactor: interactor)
        interactor.router = self
    }
    
    ...
    
    // MARK: - Private
    
    private let ticTacToeBuilder: TicTacToeBuildable

     

    RIBs는 계속해서 컴파일러가 다음 스텝을 체크(?) 해줍니다. 호다닥 가서 에러를 또 고쳐줍니다.
    builder로 가서 만들고 넣어줍니다. 

    // in LoggedInBuilder
    
    func build(withListener listener: LoggedInListener) -> LoggedInRouting {
        let component = LoggedInComponent(dependency: dependency)
        let interactor = LoggedInInteractor()
        interactor.listener = listener
        
        let offGameBuilder = OffGameBuilder(dependency: component)
        let ticTacToeBuilder = TicTacToeBuilder(dependency: component)
        return LoggedInRouter(interactor: interactor,
                              viewController: component.loggedInViewController,
                              offGameBuilder: offGameBuilder,
                              ticTacToeBuilder: ticTacToeBuilder)
    }

     

    componet가 TicTacToe Dependency를 따르고 있어야겠죠?

     

    builder까지 준비가 됐으면 route 를 구현합니다.
    build 해줄 때 Listener를 넣어줘야 하니 LoggedIn이 TicTacToe를 채택할 수 있도록 추가도 해줍니다.

    protocol LoggedInInteractable: Interactable, OffGameListener, TicTacToeListener {
        var router: LoggedInRouting? { get set }
        var listener: LoggedInListener? { get set }
    }
    
    // ... in LoggedInRouter
    
    // MARK: - LoggedInRouting
    
    func routeToTicTacToe() {
        detachCurrentChild()
        attachTicTacToe()
    }
    
    // MARK: - Private
    
    private func attachTicTacToe() {
        let ticTacToe = ticTacToeBuilder.build(withListener: interactor)
        self.currentChild = ticTacToe
        attachChild(ticTacToe)
        viewController.present(viewController: ticTacToe.viewControllable)
    }
    
    private func detachCurrentChild() {
        if let currentchild = currentChild {
            detachChild(currentchild)
            viewController.dismiss(viewController: currentchild.viewControllable)
        }
    }

     

    TicTacToeListener를 채택해줬으니 여기에 있는 didEndGame을 구현해줘야 하는데요, 이건 잠시 빈 구현체로 두고 넘어가주세요 :)
    여기까지 하면 Start Game을 눌러 TicTacToe를 할 수 있게됩니다.

     

    승자가 결정되면 OffGame으로 

    게임이 끝났을 경우도 마저 구현해보겠습니다. 

    이번엔 TicTacToe에서 OffGame으로 가야하니 routeToTicTacToe를 먼저 선언해줍니다.

    protocol LoggedInRouting: Routing {
        func routeToTicTacToe()
        func routeToOffGame()
        func cleanupViews()
    }

    이제 아까 빈 구현체로 뒀던 didEndGame을 채워줍니다.

    // MARK: - TicTacToeListener
    
    func gameDidEnd() {
        router?.routeToOffGame()
    }

    구현도 해줘야겠죠?

    // MARK: - LoggedInRouting
    
    func routeToOffGame() {
        detachCurrentChild()
        attachOffGame()
    }
    
    // MARK: - Private
    
    private func attachOffGame() {
        let offGame = offGameBuilder.build(withListener: interactor)
        self.currentChild = offGame
        attachChild(offGame)
        viewController.present(viewController: offGame.viewControllable)
    }

     

    이렇게 하면 게임이 끝나고 Close 했을 때 다시 OffGame 상태로 돌아갑니다 :]

     

    Unit testing

    튜토리얼에서 UnitTest 셈플을 제공하고 있습니다. 

    아래와 같이 Unit Testing Bundle을 추가해줍니다. 이미 만들고 찍은 사진이긴 하지만 프로젝트로 가서 + 버튼을 눌러 추가합니다.

    기본 Mock 파일들도 친절히 제공해주고 있습니다. 감사히 복붙하고 Root 폴더를 만든 뒤 RootRouterTests를 만들어주겠습니다.

    그리고 여기에 대한 샘플 테스트 코드들도 제공해주네요 !

    @testable import TicTacToe
    import XCTest
    
    class RootRouterTests: XCTestCase {
    
        private var loggedInBuilder: LoggedInBuildableMock!
        private var rootInteractor: RootInteractableMock!
        private var rootRouter: RootRouter!
    
        override func setUp() {
            super.setUp()
    
            loggedInBuilder = LoggedInBuildableMock()
            rootInteractor = RootInteractableMock()
            rootRouter = RootRouter(interactor: rootInteractor,
                       viewController: RootViewControllableMock(),
                       loggedOutBuilder: LoggedOutBuildableMock(),
                       loggedInBuilder: loggedInBuilder)
        }
    
        func test_routeToLoggedIn_verifyInvokeBuilderAttachReturnedRouter() {
            let loggedInRouter = LoggedInRoutingMock(interactable: LoggedInInteractableMock())
            var assignedListener: LoggedInListener? = nil
            loggedInBuilder.buildHandler = { (_ listener: LoggedInListener) -> (LoggedInRouting) in
                assignedListener = listener
                return loggedInRouter
            }
    
            XCTAssertNil(assignedListener)
            XCTAssertEqual(loggedInBuilder.buildCallCount, 0)
            XCTAssertEqual(loggedInRouter.loadCallCount, 0)
    
            rootRouter.routeToLoggedIn(withPlayer1Name: "1", player2Name: "2")
    
            XCTAssertTrue(assignedListener === rootInteractor)
            XCTAssertEqual(loggedInBuilder.buildCallCount, 1)
            XCTAssertEqual(loggedInRouter.loadCallCount, 1)
        }
    }

    RIB는 서로서로 Protocol을 사용해 의존성을 역전시켜주고 있어서 쉽게 테스팅이 가능합니다 :)

     

    이때 Test에서 framework들을 import할 수 없어서 에러가 나실 수 있는데요, Podfile에 Test target을 설정해줘야 합니다.

    저는 공통 pods을 정의해서 지정해줬습니다.

    use_frameworks!
    inhibit_all_warnings!
    
    def common_pods
      pod 'RIBs', :git=> 'https://github.com/uber/RIBs', :tag => '0.9.2'
      pod 'RxSwift'
      pod 'RxRelay'
      pod 'SnapKit', '~> 4.0.0'
      pod 'RxCocoa'
    end
    
    target 'TicTacToe' do
      common_pods
    end
    
    target 'TicTacToeTests' do
      common_pods
    end

     

    이때 이런 문구들이 뜨면서 설치가 안 되시는 경우가 있으실 수도 있는데요 (저처럼)

    [!] Unable to determine Swift version for the following pods:
    
    - `RIBs` is integrated by multiple targets that use a different Swift version: `TicTacToe` (Swift 4.0) and `TicTacToeTests` (Swift 5.0).
    - `RxCocoa` is integrated by multiple targets that use a different Swift version: `TicTacToe` (Swift 4.0) and `TicTacToeTests` (Swift 5.0).
    - `RxRelay` is integrated by multiple targets that use a different Swift version: `TicTacToe` (Swift 4.0) and `TicTacToeTests` (Swift 5.0).
    - `RxSwift` is integrated by multiple targets that use a different Swift version: `TicTacToe` (Swift 4.0) and `TicTacToeTests` (Swift 5.0).
    

    Uber의 RIB 사이트에서 프로젝트를 그대로 가져오셨다면 스위프트 버전이 Swift4로 설정되어 있고, 새로 만든 Test는 Swift5로 설정되어 있어서 문제가 될 수 있습니다. TicTacToe 프로젝트의 스위프트 버전을 똑같이 맞춰주세요 :)

    아니 같은 걸 설치하라는데 왜 버전이 다른거야!! 라고 하면서 Pod에 문제가 있다고 생각하고 이것저것 엄청 삽질을 했었네요..
    스트레스..... 🤦🏻‍♂️

     

    튜토리얼 3편에서 뵙겠습니다 !

    감사합니다.

     

    'iOS' 카테고리의 다른 글

    Playground using Algorithms Pakage  (0) 2021.10.24
    RIBs) tutorial3  (0) 2021.06.26
    RIBs) tutorial2  (1) 2021.06.26
    RIBs) tutorial1  (0) 2021.06.25
    CALayer 그리고 View와의 관계  (0) 2021.01.31

    댓글

Designed by Tistory.