본문 바로가기
🍎 iOS/Architecture

[RIBs] Router를 이용해 화면 전환해보기 (feat. Memo)

by Fomagran 💻 2021. 8. 24.
728x90
반응형

 

안녕하세요 Foma👟 입니다!

 

오늘은 RIBs 아키텍처를 사용하여 화면 전환하는 법에 대해서 알아보도록 하겠습니다.

 

기존 Uber의 튜토리얼을 따라하려고 했으나... 저는 잘안되더라구요...

 

그래서 다른 좋은 튜토리얼이 없을까? 하고 열심히 찾아보다가 SimpleMemo를 RIBs로 구현한 튜토리얼 찾게되었습니다.

 

 

GitHub - eunjin3786/SimpleMemo-RIBs: SimpleMemo를 RIBs로 바꿔보자 :-)

SimpleMemo를 RIBs로 바꿔보자 :-). Contribute to eunjin3786/SimpleMemo-RIBs development by creating an account on GitHub.

github.com

 

전 포스팅에서 RootRIB에서 LoggedOutRIB을 붙여 이동하고 LoggedOutRIB에서 간단한 비지니스 로직을 처리했는데요.

 

(혹시 저번 포스팅을 못보신 분들은 여기 에서 보고 와주세요!)

 

이어서 RootRIB에서 LoggedOutRIB을 떼고 LoggedInRIB을 붙이고 LoggedInRIB 하위에 있는 MemoRIB을

 

화면에 띄워보도록 하겠습니다.

 

상태 트리를 간략히 보여드리면 아래와 같습니다.

 

 

바로 시작할게요~


LoggedOut

 

저번 시간에 LoggedOutRIB에서 로그인 버튼을 누르면 Player1과 Player2의 이름을 출력하도록 했는데요.

 

이번에는 로그인 버튼을 누르면 LoggedOut이 dismiss되고 RootRIB으로 다시 돌아가는 것을 구현해보겠습니다.

 

LoggedOutInteractor

 

LoggedOutListener 프로토콜에 didLogin 함수를 정의해줍니다.

 

protocol LoggedOutListener: AnyObject {
    func didLogin(player1Name:String,player2Name:String)
}

 

그리곤 LoggedOutInteractor의 handleLogind에서 listener를 통해 didLogin을 했다고 상위 RIB인 RootRIB에 알려줍니다.

 

 func handleLogin(player1Name: String, player2Name: String) {
        listener?.didLogin(player1Name: player1Name, player2Name: player2Name)
    }

 

RootRouter

 

RootRouter엔 RootInteractable 프로토콜이 정의되어 있는데 여기서 LoggedOutListener를 채택하여 

 

LoggedOutListener에서 발생한 이벤트를 RootIneractor에서 감지할 수 있도록 합니다.

 

protocol RootInteractable: Interactable, LoggedOutListener {
    var router: RootRouting? { get set }
    var listener: RootListener? { get set }
}

 

RootInteractor

 

RootInteractor로 가시면 아래와 같이 RootIneractable을 채택하고 있어요.

 

그러면 당연히 RootInteractable에서 채택한 LoggedOutListener의 메소드도 구현해줘야겠죠?

 

 

이미 정의해둔 didLogin이 자동완성 될거에요.

 

여기에 이제 로그인을 했을 때 어떤 것을 할지 구현해줘야 하는데요.

 

func didLogin(player1Name: String, player2Name: String) {
	...
    }

 

RootRouting에 로그인된 화면으로 인도하는 메소드를 정의해주겠습니다.

 

protocol RootRouting: ViewableRouting {
    func routeToLoggedIn(player1Name:String,player2Name:String)
}

 

그리곤 didLogin 메소드에 위에서 정의해줬던 routeToLoggedIn을 실행시켜줍니다.

 

 func didLogin(player1Name: String, player2Name: String) {
        router?.routeToLoggedIn(player1Name: player1Name, player2Name: player2Name)
    }

 

RootRouter

 

이제 RootRouter로 이동해서 routeToLoggedIn을 구현해줘야겠죠?

 

먼저 RootViewControllable 프로토콜에 화면을 띄우고 사라지게 하는 present,dismiss 메소드를 정의하겠습니다.

 

protocol RootViewControllable: ViewControllable {
    func present(viewController: ViewControllable)
    func dismiss(viewController:ViewControllable)
}

 

그리곤 RootRouter에 RootInteractor에서 실행시킨 routeToLoggedIn 메소드를 구현해줄건데요.

 

로그인 화면으로 이동하려면 먼저 기존에 붙여져있던 loggedOut 자식을 떼어줘야겠죠?

 

detachChild 메소드를 통해 떼어줍니다.

 

그리고 RootViewControllable 프로토콜에서 정의해둔 dismiss 메소드를 이용해서 loggedOut를 dismiss시켜주고

 

loggedOut을 nil로 만들어줍니다.

 

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

 

이렇게 하면 로그인 버튼을 눌렀을 때 LoggedOut이 dismiss되고 RootRIB으로 되돌아가게 됩니다.

 


LoggedIn

 

이제 LoggedOutRIB을 떼어줬고 LoggedIn을 붙여줘야겠죠?

 

LoggedInRIB을 만들어줍니다.

 

주의하실 점은 LoggedIn은 어떤 것을 보여주는 것이 아닌 로그인한 상태만 알려주는 RIB이기 때문에 View를 갖지 않습니다.

 

고로 Owns corresponding view 체크를 해제해주고 만들어주세요.

 

 

이렇게 만들게 되면 아래와 같이 Router,Builder,Interactor 3개 파일만 생성될거에요.

 

 

RootComponent + LoggedIn

 

RootComponent + LoggedIn 이름으로 swift파일을 생성해주세요.

 

그리곤 RootComponent에 LoggedIn Dependency를 주입해주겠습니다.

 

LoggedInDependency를 채택할 땐 화면이 없는 RIB이기 떄문에 LoggedInViewController를 rootViewController로 지정해줘야 합니다.

 

(return rootViewController에서 오류가 날거에요.  아직 RootViewController에 LoggedInViewControllable을 채택하지 않았기 때문입니다.)

 

import RIBs

protocol RootDependencyLoggedIn: Dependency {
}

extension RootComponent: LoggedInDependency {
    var LoggedInViewController: LoggedInViewControllable {
        return rootViewController
    }
}

 

RootViewController

 

RootViewController로 이동해서 LoggedInViewControllable을 확장시켜줍니다.

 

// MARK: LoggedInViewControllable

extension RootViewController: LoggedInViewControllable {
}

 

RootBuilder

 

Builder의 역할은 RIB을 생성해주는 것입니다.

 

고로 RootBuilder로 이동하여 LoggedInBuilder를 생성해줍니다.

 

(return RootRouter 부분에서 에러가 날거에요. 아직 RootRouter에 loggedInBuilder 초기화를 안해주었기 때문입니다.)

 

func build() -> LaunchRouting {
           let viewController = RootViewController()
           let component = RootComponent(dependency: dependency,
                                         rootViewController: viewController)
           let interactor = RootInteractor(presenter: viewController)

           let loggedOutBuilder = LoggedOutBuilder(dependency: component)
           let loggedInBuilder = LoggedInBuilder(dependency: component)
        
           return RootRouter(interactor: interactor,
                             viewController: viewController,
                             loggedOutBuilder: loggedOutBuilder,
                             loggedInBuilder:loggedInBuilder)
       }

 

RootRouter

 

RootRouter로 이동하여 loggedInBuilder를 선언해주시고

 

private let loggedInBuilder:LoggedInBuildable

 

loggedInBuilder를 초기화해줍니다.

 

init(interactor: RootInteractable,
         viewController: RootViewControllable,
         loggedOutBuilder: LoggedOutBuildable,
         loggedInBuilder:LoggedInBuildable) {
        
        self.loggedOutBuilder = loggedOutBuilder
        self.loggedInBuilder = loggedInBuilder
        
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }

 

그리곤 자식의 이벤트를 듣기 위해 RootIneractable LoggedInListener도 채택해줍니다.

 

protocol RootInteractable: Interactable, LoggedOutListener, LoggedInListener  {
        var router: RootRouting? { get set }
        var listener: RootListener? { get set }
    }

 

그리곤 routeToLoggedIn 함수에 loggedInRouting을 RootRIB의 자식으로 붙여줍니다.

 

 func routeToLoggedIn(player1Name: String, player2Name: String) {
        if let loggedOut = loggedOut {
            detachChild(loggedOut)
            viewController.dismiss(viewController:loggedOut.viewControllable)
            self.loggedOut = nil
        }
        let loggedInRouting = loggedInBuilder.build(withListener: interactor)
        attachChild(loggedInRouting)
    }

 

이렇게 실행해보면 아무것도 일어나지 않을거에요.

 

왜냐하면 LoggedInRIB은 뷰가 없기 때문이죠.


Memo

 

LoggedInRIB의 자식인 MemoRIB을 만들어주겠습니다.

 

Memo는 메모 화면을 구성할 RIB입니다.

 

고로 뷰를 가지고 있고 스토리보드로 화면을 구성할 것이기 때문에

 

Owns corresponding view와 Adds Storyboard file을 체크해주세요.

 

 

 

 

LoggedInComponent + Memo

 

이제 MemoRIB을 LoggedInRIB의 자식으로 설정해줘야겠죠?

 

먼저 LoggedInComponent에 MemoDependency를 주입해주겠습니다.

 

import RIBs

protocol LoggedInDependencyMemo: Dependency {
}

extension LoggedInComponent: MemoDependency {
}

 

LoggedInBuilder

 

RootRIB에서 LoggedInRIB을 자식으로 설정했던 것과 같이 LoggedInBuilder로 이동해서 MemoRIB을 생성해줍니다.

 

func build(withListener listener: LoggedInListener) -> LoggedInRouting {
        let component = LoggedInComponent(dependency: dependency)
        let interactor = LoggedInInteractor()
        interactor.listener = listener
        let memoBuilder = MemoBuilder(dependency: component)
        return LoggedInRouter(interactor: interactor,
                              viewController: component.LoggedInViewController,
                              memoBuilder:memoBuilder)
    }

 

LoggedInRouter

 

LoggedInRouter에 memoBuilder를 초기화해줍니다.

 

final class LoggedInRouter: Router<LoggedInInteractable>, LoggedInRouting {

    private let memoBuilder:MemoBuildable
    
    // TODO: Constructor inject child builder protocols to allow building children.
    init(interactor: LoggedInInteractable, viewController: LoggedInViewControllable,memoBuilder:MemoBuildable) {
        self.viewController = viewController
        self.memoBuilder = memoBuilder
        super.init(interactor: interactor)
        interactor.router = self
    }

 

그리곤 LoggedInInteractable 프로토콜에 MemoListener를 채택해준 뒤

 

protocol LoggedInInteractable: Interactable,MemoListener {
    var router: LoggedInRouting? { get set }
    var listener: LoggedInListener? { get set }
}

 

LoggedInRouter에 didLoad 메소드를 만들어 로드되는 순간 MemoRIB을 붙여줍니다.

 

override func didLoad() {
        super.didLoad()
        let memoRouting = memoBuilder.build(withListener: interactor)
        attachChild(memoRouting)
    }

 

이제 MemoRIB의 화면을 보여줘야겠죠?

 

LoggedInViewControllable 프로토콜에 present 메소드를 정의합니다.

 

protocol LoggedInViewControllable: ViewControllable {
    func present(viewController: ViewControllable)
}

 

다시 didLoad 메소드로 와서 memoRouting을 present 해주도록 구현해줍니다.

 

override func didLoad() {
        super.didLoad()
        let memoRouting = memoBuilder.build(withListener: interactor)
        attachChild(memoRouting)
        viewController.present(viewController: memoRouting.viewControllable)
    }

 

MemoBuilder

 

이제 마지막으로 MemoBuilder로 이동해서 Storyboard를 불러오도록 구현해줍니다.

 

func build(withListener listener: MemosListener) -> MemosRouting {
            let component = MemosComponent(dependency: dependency)
            let viewController = UIStoryboard(name: "MemosViewController", bundle: nil).instantiateInitialViewController() as! MemosViewController
            let interactor = MemosInteractor(presenter: viewController)
            interactor.listener = listener
            return MemosRouter(interactor: interactor, viewController: viewController)
        }

 

 

StoryboardInstantiate

 

위와 같이 코드를 쓸 수도 있지만 조금 더 개선해주기 위해 Storyboard enum을 만들어 주겠습니다.

 

import UIKit

enum Storyboard: String {
    case MemoViewController
    
    func instantiate<VC: UIViewController>(_: VC.Type) -> VC {
        guard let vc = UIStoryboard(name: self.rawValue, bundle: nil).instantiateInitialViewController() as? VC else {
            fatalError("Storyboard \(self.rawValue) wasn`t found.")
        }
        return vc
    }
}

 

MemoBuilder

 

다시 MemoBuilder로 돌아와 build의 코드를 개선해줍니다.

 

 func build(withListener listener: MemoListener) -> MemoRouting {
        let component = MemoComponent(dependency: dependency)
        let viewController = MemoViewController.instantiate()
        let interactor = MemoInteractor(presenter: viewController)
        interactor.listener = listener
        return MemoRouter(interactor: interactor, viewController: viewController)
    }

 

MemoViewController

 

MemoViewController로 이동해 스토리보드를 instantiate를 해줍니다.

 

final class MemoViewController: UIViewController, MemoPresentable, MemoViewControllable {
    
    static func instantiate() -> Self {
        return Storyboard.MemoViewController.instantiate(self)
    }
    
    weak var listener: MemoPresentableListener?
}

 

그리곤 MemoViewController라는 것을 알려주기 위해 Label을 달아주겠습니다.

 

스토리보드에 label을 달아주고

 

 

MemoViewController에 연결해준 뒤

 

viewDidLoad 함수를 만들어 label의 텍스트를 "여기는 메모뷰컨트롤러 입니다!" 라고 알려주도록 하겠습니다.

 

final class MemoViewController: UIViewController, MemoPresentable, MemoViewControllable {
    
    @IBOutlet weak var memoLabel: UILabel!
    
    static func instantiate() -> Self {
        return Storyboard.MemoViewController.instantiate(self)
    }
    
    override func viewDidLoad() {
        memoLabel.text = "여기는 메모뷰컨트롤러 입니다!"
    }
    
    weak var listener: MemoPresentableListener?
}

실행화면

 

이렇게 실행해서 확인해보면 메모 뷰컨트롤러로 이동해되는 모습을 볼 수 있습니다!!

 

 


오늘은 이렇게 LoggedOutRIB에서 로그인 버튼을 누르면 LoggedOutRIB이 detach되고 RootRIB에서 LoggedInIRIB이 

 

attach 되고 LoggedInRIB의 자식인 MemoRIB이 보여지는 것을 구현해보았습니다.

 

만약 기존의 MVC패턴으로 구현했다면 정말 간단하게 구현할 수 있는 화면 전환을 RIBs 아키텍처로 구현하니 정말 많은

 

파일이 생성되고 엄청 복잡하게 설계해야 화면 전환을 할 수 있었습니다.

 

하지만 그만큼 의존성이나 메모리 프로토콜 지향 프로그래밍을 하게 되어 안정성을 보장되는 설계를 할 수 있었습니다.

 

또한 RIB이 상태를 나열하고 붙였다 뗐다 하면서 상태를 이어가기 때문에 다른 사람과 협업 그리고 기능 확장 등을 할 때

 

유용하겠다라는 생각이 들었습니다.

 

혹시라도 틀린 점이 있거나 궁금한 점이 있다면 언제든 댓글로 알려주세요!

728x90
반응형

댓글