RIBs - 뷰가 없는 RIB (3)

김재형_LittleTale·2025년 10월 1일

RIBs

목록 보기
4/4
post-thumbnail

들어가기에 앞서

이번시간에는 저번시간에 이어서 프로젝트를 이어가 보곘습니다.
저번시간에 로그인 기능을 가지고 뷰를 가진 RIB 을 구성하였습니다.
이번에는 HomeRIBRandomRIB 이 둘을 중계하는 Flow RIB을 구성 해보겠습니다.

Home RIB

빠른 진행을 위해 UI는 간단하게 진행합니다.

HomeViewController

final class HomeViewController: UIViewController, HomePresentable, HomeViewControllable {

    weak var listener: HomePresentableListener?
    /// Root -> Login -> Name Label
    private let nameLabel = UILabel().after {
        $0.font = .systemFont(ofSize: 20, weight: .bold)
        $0.textColor = .black
        $0.translatesAutoresizingMaskIntoConstraints = false
    }
    
    private let backLoginButton = UIButton().after {
        $0.setTitle("Go Back", for: .normal)
        $0.setTitleColor(.red, for: .normal)
        $0.translatesAutoresizingMaskIntoConstraints = false
    }
    
    private let randomColorMoveButton = UIButton().after {
        $0.setTitle("NEXT", for: .normal)
        $0.setTitleColor(.black, for: .normal)
        $0.translatesAutoresizingMaskIntoConstraints = false
    }
    
    override func loadView() {
        super.loadView()
        self.view.backgroundColor = .blue
        setViewHierarchy()
        setUI()
        subscribe()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = listener?.getName()
    }
    
    func pushViewController(_ viewController: ViewControllable) {
        self.navigationController?.pushViewController(viewController.uiviewController, animated: true)
    }
    
    func popViewController(animated: Bool) {
        self.navigationController?.popViewController(animated: animated)
    }
}

extension HomeViewController {
    
    private func setViewHierarchy() {
        view.addSubview(nameLabel)
        view.addSubview(backLoginButton)
        view.addSubview(randomColorMoveButton)
    }
    
    private func setUI() {
        NSLayoutConstraint.activate([
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            
            backLoginButton.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 10),
            backLoginButton.widthAnchor.constraint(equalToConstant: 120),
            backLoginButton.heightAnchor.constraint(equalToConstant: 40),
            
            randomColorMoveButton.topAnchor.constraint(equalTo: backLoginButton.bottomAnchor, constant: 10),
            randomColorMoveButton.centerXAnchor.constraint(equalTo: backLoginButton.centerXAnchor)
        ])
    }
}

extension HomeViewController {

    private func subscribe() {
        backLoginButton.rx.tap
            .bind(with: self) { owner, _ in
                owner.listener?.goBackToLogin()
            }
            .disposed(by: rx.disposeBag)
        
        randomColorMoveButton.rx.tap
            .bind(with: self) { owner, _ in
                owner.listener?.moveToRandomView()
            }
            .disposed(by: rx.disposeBag)
    }
}

PresentableListener

protocol HomePresentableListener: AnyObject {
    func getName() -> String
    
    func goBackToLogin()
    
    func moveToRandomView()
}

Home Router

protocol HomeInteractable: Interactable, RandomListener {
    var router: HomeRouting? { get set }
    var listener: HomeListener? { get set }
}

protocol HomeViewControllable: ViewControllable {
    // TODO: Declare methods the router invokes to manipulate the view hierarchy.
    func pushViewController(_ viewController: ViewControllable)
    
    func popViewController(animated: Bool)
}

final class HomeRouter: ViewableRouter<HomeInteractable, HomeViewControllable>, HomeRouting {

    private let randomColorBuilder: RandomBuilder
    private var randomRouting: RandomRouting?
    
    // TODO: Constructor inject child builder protocols to allow building children.
    init(interactor: HomeInteractable,
         viewController: HomeViewControllable,
         randomColorBuilder: RandomBuilder
    ) {
        self.randomColorBuilder = randomColorBuilder
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
    
    func routeToRandomColorView() {
        let child = randomColorBuilder.build(withListener: interactor)
        attachChild(child)
        self.randomRouting = child
        viewController.pushViewController(child.viewControllable)
    }
    
    func backToHome() {
        guard let child = randomRouting else { return }
        viewController.popViewController(animated: true)
        detachChild(child)
        randomRouting = nil
    }
}

Home Interactor

protocol HomeRouting: ViewableRouting {
    // TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
    func routeToRandomColorView()
    
    func backToHome()
}

protocol HomePresentable: Presentable {
    var listener: HomePresentableListener? { get set }
    // TODO: Declare methods the interactor can invoke the presenter to present data.
}

protocol HomeListener: AnyObject {
    // TODO: Declare methods the interactor can invoke to communicate with other RIBs.
    
    func didRequestLogin()
}

final class HomeInteractor: PresentableInteractor<HomePresentable>, HomeInteractable, HomePresentableListener {
   
    weak var router: HomeRouting?
    weak var listener: HomeListener?
    private let name: String

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    init(presenter: HomePresentable, name: String) {
        self.name = name
        super.init(presenter: presenter)
        presenter.listener = self
    }

    override func didBecomeActive() {
        super.didBecomeActive()
        // TODO: Implement business logic here.
    }

    override func willResignActive() {
        super.willResignActive()
        // TODO: Pause any business logic.
    }
    
    func getName() -> String {
        return self.name
    }
    
    func goBackToLogin() {
        listener?.didRequestLogin()
    }
    
    func moveToRandomView() {
        router?.routeToRandomColorView()
    }
    
    func randomBack() {
        router?.backToHome()
    }
}

HomeBuilder

protocol HomeDependency: Dependency {
    // TODO: Declare the set of dependencies required by this RIB, but cannot be
    // created by this RIB.
}

final class HomeComponent: Component<HomeDependency> {

    // TODO: Declare 'fileprivate' dependencies that are only used by this RIB.
    
    let name: String
    
    init(dependency: HomeDependency, name: String) {
        self.name = name
        super.init(dependency: dependency)
    }
}

extension HomeComponent: RandomDependency {
    
}

// MARK: - Builder

protocol HomeBuildable: Buildable {
    func build(withListener listener: HomeListener, name: String) -> HomeRouting
}

final class HomeBuilder: Builder<HomeDependency>, HomeBuildable {

    override init(dependency: HomeDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: HomeListener, name: String) -> HomeRouting {
        let component = HomeComponent(dependency: dependency, name: name)
        let viewController = HomeViewController()
        let interactor = HomeInteractor(presenter: viewController, name: name)
        
        let randomBuilder = RandomBuilder(dependency: component)
        
        interactor.listener = listener
        return HomeRouter(interactor: interactor, viewController: viewController, randomColorBuilder: randomBuilder)
    }
}

자 저번 시간에 각각의 역활을 다루면서 하였기에 설명은 생략하겠습니다.
중요한 핵심은 뷰 이동을 수행하는 역활은 이친구가 하긴 합니다.
그 판단은 여기서 하지 않습니다.

Random RIB

Rib 구성은 길어짐으로 생략하고 UI 만 작성해 놓겠습니다.
한번 직접 구성해 보시는 것을 추천합니다.

import UIKit
import RxSwift
import RxCocoa

final class RandomView: UIView {
    
    private let colorChangeButton = UIButton().after { b in
        b.setTitle("색상 바꾸기", for: .normal)
        b.setTitleColor(.cyan, for: .normal)
        b.backgroundColor = .lightGray
        b.titleLabel?.font = .boldSystemFont(ofSize: 24)
        b.translatesAutoresizingMaskIntoConstraints = false
    }
    
    private let backButton = UIButton().after {
        $0.setTitle("BACK", for: .normal)
        $0.backgroundColor = .lightGray
        $0.setTitleColor(.red, for: .normal)
        $0.translatesAutoresizingMaskIntoConstraints = false
    }
    
    // MARK: Internal
    enum RandomViewEvent {
        case colorChange
        case backButtonTapped
    }
    
    let viewEventStream = PublishRelay<RandomViewEvent> ()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUI()
        subscribeUIEvent()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// MARK: UI
extension RandomView {

    private func setUI() {
        self.backgroundColor = .random
        self.addSubview(backButton)
        self.addSubview(colorChangeButton)
        
        NSLayoutConstraint.activate([
            colorChangeButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40),
            colorChangeButton.centerXAnchor.constraint(equalTo: centerXAnchor),
            colorChangeButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
            
            backButton.topAnchor.constraint(equalTo: colorChangeButton.bottomAnchor, constant: 20),
            backButton.centerXAnchor.constraint(equalTo: centerXAnchor),
        ])
    }
}

// MARK: UI Event
extension RandomView {

    private func subscribeUIEvent() {
        colorChangeButton.addAction(UIAction(handler: { [weak self] _ in
            guard let owner = self else { return }
            owner.viewEventStream.accept(.colorChange)
        }), for: .touchDown)
        
        backButton.rx.tap
            .bind(with: self) { owner, _ in
                owner.viewEventStream.accept(.backButtonTapped)
            }
            .disposed(by: rx.disposeBag)
    }
}

Flow RIB (핵심)

지금은 뷰 구조가 단순하니까 Flow RIB 형태가 필요 하지 않습니다.
다만 뷰가 많다고 가정해 보겠습니다.
뷰 이동 관련해서 저희는 복잡한 뷰이동은 다른 파일에서 관리 했었을 겁니다.

코디네이터 패턴을 쓰거나, 라우터 패턴을 쓰거나 해서 관리를 하였는데
이것도 비슷한 방법이다 라고 생각하시면서
핵심은 뷰를 가지고 있는 않는 RIB을 통해 관리할 예정이니 집중 바랍니다.

Home Flow ViewController? -> X

없습니다.!!

Home Flow Interactor

뷰가 없는데 뷰모델이 존재합니다 만 거의 안씁니다.
하지만 각 하위의 리스너를 감지하게 해야 합니다.

final class HomeFlowInteractor: Interactor, HomeFlowInteractable {

    weak var router: HomeFlowRouting?
    weak var listener: HomeFlowListener?

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    override init() {}

    override func didBecomeActive() {
        super.didBecomeActive()
        // TODO: Implement business logic here.
    }

    override func willResignActive() {
        super.willResignActive()
    }
}

extension HomeFlowInteractor: HomeListener {
    func didRequestLogin() {
        listener?.moveToLogin()
    }
}

extension HomeFlowInteractor: RandomListener {
    func randomBack() {
        router?.routeToRandomColor()
    }
}

protocol HomeFlowRouting: Routing {
    func start(name: String)
    func routeToRandomColor()
    func routeBackToHome()
}

protocol HomeFlowListener: AnyObject {
    // TODO: Declare methods the interactor can invoke to communicate with other RIBs.
    
    func moveToLogin()
}

Home Flow Router

RIB이 직접 뷰컨트롤러를 가지진 않지만 주입을 받을 겁니다.

protocol HomeFlowInteractable: Interactable, HomeListener, RandomListener {
    var router: HomeFlowRouting? { get set }
    var listener: HomeFlowListener? { get set }
}

final class HomeFlowRouter: Router<HomeFlowInteractable>, HomeFlowRouting {

    // MARK: - Private
    private let rootVc: RootViewControllable
    private let homeBuilder: HomeBuildable
    private let randomColorBuilder: RandomBuildable
    
    private var homeRouting: HomeRouting?
    private var randomColorRouting: RandomRouting?
    
    init(interactor: HomeFlowInteractable, rootVc: RootViewControllable, homeBuilder: HomeBuildable, randomColorBuilder: RandomBuildable, homeRouting: HomeRouting? = nil, randomColorRouting: RandomRouting? = nil) {
        self.rootVc = rootVc
        self.homeBuilder = homeBuilder
        self.randomColorBuilder = randomColorBuilder
        self.homeRouting = homeRouting
        self.randomColorRouting = randomColorRouting
        super.init(interactor: interactor)
        interactor.router = self
    }
    
    func start(name: String) {
        let home = homeBuilder.build(withListener: interactor, name: name)
        attachChild(home)
        
        homeRouting = home
        rootVc.setRoot(home.viewControllable, animated: false)
    }
    
    func routeToRandomColor() {
        guard homeRouting != nil else { return }
        let random = randomColorBuilder.build(withListener: interactor)
        attachChild(random)
        randomColorRouting = random
        rootVc.push(random.viewControllable, animated: true)
    }
    
    func routeBackToHome() {
        guard let random = self.randomColorRouting else { return }
        rootVc.pop(animated: true)
        detachChild(random)
        randomColorRouting = nil
    }
    
}

Home Flow Builder

protocol HomeFlowDependency: Dependency {
    var rootVC: RootViewControllable { get }
    var homeBuilder: HomeBuildable { get }
    var randomBuilder: RandomBuildable { get }
}

final class HomeFlowComponent: Component<HomeFlowDependency> {
    
}

// MARK: - Builder
protocol HomeFlowBuildable: Buildable {
    func build(withListener listener: HomeFlowListener) -> HomeFlowRouting
}

final class HomeFlowBuilder: Builder<HomeFlowDependency>, HomeFlowBuildable {

    override init(dependency: HomeFlowDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: HomeFlowListener) -> HomeFlowRouting {
        let component = HomeFlowComponent(dependency: dependency)
        let interactor = HomeFlowInteractor()
        interactor.listener = listener
        
//        return HomeFlowRouter(interactor: interactor, viewController: component.HomeFlowViewController)
        return HomeFlowRouter(interactor: interactor, rootVc: dependency.rootVC, homeBuilder: dependency.homeBuilder, randomColorBuilder: dependency.randomBuilder)
    }
}

다음과 같이 의존성을 두어서 진행하겠습니다.

Root Builder

final class RootBuilder: Builder<RootDependency>, RootBuildable {

    override init(dependency: RootDependency) {
        super.init(dependency: dependency)
    }

    func build() -> LaunchRouting {
        let viewController = RootViewController()
        // 이부분에서 
        let component = RootComponent(dependency: dependency, rootVc: viewController)
        
        let interactor = RootInteractor(presenter: viewController)
        
        let loginBuilder    = LoginBuilder(dependency: component)
        let homeFlowBuilder = HomeFlowBuilder(dependency: component)
        
        return RootRouter(
            interactor: interactor,
            viewController: viewController,
            loginBuilder: loginBuilder,
            homeFlowBuilder: homeFlowBuilder
        )
    }
}

RootViewController 에서 push, pop만 구성해놓으면 (프로토콜에 의해서)
구조가 잡히게 됩니다.

마무리 하면서

RIBs 편이 마무리가 되었습니다.
글이 조금 부실하게 느껴질거라 생각합니다.
코드로 설명할게 많다 보니 오히려 말로 풀어내기가 애매하다고 생각을 합니다.

제가 작성한 코드를 복붙 하지 마시고 하나하나 적으면서 생각하시면 좋지 않을까 생각이 듭니다.
사실 말로 설명은 1편에서 개념편에서 한번에 정리한게 끝입니다.
어떻게 활용하냐에서는 코드가 많은게 더 좋을 것 같아서 순서 정도만 잡으면서 작성한 것 같구요

RIBs 도 상당히 아이디어는 좋은 아키텍처라고 생각합니다.
하지만 난이도와 ASAP 한 환경을 생각해본다면 비추가 되는 아키텍처라고 생각을 합니다.

정말 고생많으셨고 다음시간은
XCTest 를 다루어 볼까 합니다. ( 다룬줄 알았는데 아니더라구요 )
XCTest -> TCA 와 Testable 형태로 작성할 수도 있고
Unity 여정 을 작성할 수도 있습니다....!

Unity도 공부해보면 좋을 것 같아서...!
댓글로 원하시는거 적으시면 반영 하겠습니다...!

profile
IOS 개발자 새싹이, 작은 이야기로부터

0개의 댓글