Book_Kitty - Coordinator(02)

✋ Introduce

이번 글에서는 BookKitty 프로젝트에서 도입한 Coordinator 패턴 중 TabBarCoordinator를 어떻게 구현했는지 소개하고자 합니다.

BookKitty는 MVVM-C 아키텍처를 기반으로 개발된 앱으로, 화면 전환을 효과적으로 관리하기 위해 Coordinator 패턴을 도입했습니다. 특히, 앱의 핵심 구조 중 하나인 TabBarController 기반의 화면 전환을 다루기 위해 TabBarCoordinator를 구현했으며, 이를 통해 각 탭의 흐름을 독립적으로 관리할 수 있도록 설계했습니다.

이 글에서는 TabBarCoordinator의 역할과 구현 방식, 그리고 BookKitty에서 어떻게 활용했는지 자세히 설명하겠습니다.

TabBar Flow 설계

Book_Kitty 프로젝트는 3개의 Tab(Home, Q&A, Library)이 존재하며, 플로팅 버튼을 통한 두 개의 플로우(AddBook, AddQuestion)를 제공합니다.

Memory-structure image


사진과 같은 화면 전환을 제공하기 위해 Tab 전환에 따른 플로우 3개, FloatingButton에 따른 플로우 2개 총 5개의 독립적인 화면 전환을 Coordinator로 분리하고, TabBarCoordinator에서 각각의 Coordinator를 생성해서 자식 Coordinator로 관리하는 방식을 설계했습니다.

또한, 여러 플로우에서 공통적으로 접근하는 화면(BookDetail)은 별도의 Coordinator에서 관리할 수 있도록 설계해서 재사용성을 확보했습니다.

Memory-structure image

Coordinator 패턴 적용

먼저, TabBarCoordinator는 Coordinator 프로토콜을 따르며, 아래의 주요 속성을 가집니다.

final class TabBarCoordinator: Coordinator {
    weak var finishDelegate: CoordinatorFinishDelegate?
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    var tabBarController: TabBarController
    var tabBarViewModel: TabBarViewModel
    private let disposeBag = DisposeBag()
    
    init(_ navigationController: UINavigationController) {
        self.navigationController = navigationController
        tabBarViewModel = TabBarViewModel()
        tabBarController = TabBarController(viewModel: tabBarViewModel)
    }
}
  • tabBarController를 생성하고 네비게이션 컨트롤러에 추가합니다
  • home, qna, book 탭을 관리하는 각각의 Coordinator를 생성하고 childCoordinators에 등록합니다.
  • RxSwift를 활용해 이벤트를 감지하고, 특정 액션이 발생하면 화면 전환을 수행합니다.

start() 메서드 – Coordinator의 시작

start() 메서드는 TabBarCoordinator가 실행될 때 호출되며, 각 탭에 해당하는 Coordinator를 생성하고 tabBarController에 연결하는 핵심 역할을 합니다.

func start() {
    // 각 탭에 해당하는 Coordinator 생성
    let homeCoordinator = DefaultHomeCoordinator(navigationController)
    let qnaCoordinator = DefaultQuestionCoordinator(navigationController)
    let bookCoordinator = MyLibraryCoordinator(navigationController)

    // childCoordinators에 등록
    addChildCoordinator(homeCoordinator, qnaCoordinator, bookCoordinator)

    // 각 Coordinator 실행
    homeCoordinator.start()
    qnaCoordinator.start()
    bookCoordinator.start()

    // TabBarController에 뷰 컨트롤러 설정
    tabBarController.setViewControllers(
        homeCoordinator.homeViewController,
        qnaCoordinator.questionHistoryViewController,
        bookCoordinator.myLibraryViewController
    )

    // 책 추가 또는 질문 추가 이벤트를 감지하여 적절한 플로우 실행
    tabBarViewModel.navigateToAddBook
        .withUnretained(self)
        .bind(onNext: { owner, _ in
            owner.showAddBookFlow()
        }).disposed(by: disposeBag)

    tabBarViewModel.navigateToAddQuestion
        .withUnretained(self)
        .bind(onNext: { owner, _ in
            owner.showAddQuestionFlow()
        }).disposed(by: disposeBag)

    // 네비게이션 스택에 TabBarController 추가
    navigationController.pushViewController(tabBarController, animated: true)
}
  • Home, QnA, MyLibrary Coordinator를 생성하고 childCoordinators에 추가합니다.
  • 각 Coordinator의 start()를 호출하여 각 탭의 초기화 작업을 수행합니다.
  • tabBarController.setViewControllers()를 통해 생성한 뷰 컨트롤러들을 탭바에 등록합니다.
  • RxSwift를 활용하여 특정 이벤트(navigateToAddBook, navigateToAddQuestion)가 발생하면 적절한 화면으로 이동합니다.

특정 플로우 실행 (showAddBookFlow, showAddQuestionFlow)

책 추가 및 질문 추가와 같은 특정 이벤트 발생 시, 각각의 Coordinator를 실행하는 메서드를 정의합니다.

private func showAddBookFlow() {
    let addBookCoordinator = AddBookCoordinator(navigationController)
    addChildCoordinator(addBookCoordinator)
    addBookCoordinator.finishDelegate = self
    addBookCoordinator.start()
}

private func showAddQuestionFlow() {
    let addQuestionCoordinator = AddQuestionCoordinator(navigationController)
    addChildCoordinator(addQuestionCoordinator)
    addQuestionCoordinator.finishDelegate = self
    addQuestionCoordinator.start()
}

Coordinator 종료 처리 (coordinatorDidFinish)

Coordinator가 종료되었을 때 childCoordinators에서 제거하고 적절한 탭으로 이동합니다.

func coordinatorDidFinish(childCoordinator: Coordinator) {
    childCoordinators.removeAll { $0 === childCoordinator }
    if childCoordinator is AddQuestionCoordinator {
        tabBarController.tabBar.selectedIndex.accept(1)
    } else if childCoordinator is AddBookCoordinator {
        tabBarController.tabBar.selectedIndex.accept(2)
    }
    navigationController.popViewController(animated: true)
}

TabBarController의 역할

TabBarController는 UITabBarController를 상속하지 않고, 커스텀 UIViewController로 구현되었습니다. 이 컨트롤러는 탭 전환 로직과 플로팅 메뉴(Floating Menu)를 관리하는 역할을 합니다.

setViewControllers(_:) – 탭 관리할 뷰 컨트롤러 설정

func setViewControllers(_ viewControllers: UIViewController...) {
    self.viewControllers = viewControllers
}
  • UITabBarController의 setViewControllers(_:)와 유사하게 관리할 뷰 컨트롤러 목록을 저장합니다.
  • 가변 매개변수(UIViewController…)를 사용하여 여러 개의 뷰 컨트롤러를 전달받을 수 있도록 구현되었습니다.

setupInitialViewController() – 첫 번째 탭을 기본으로 설정

private func setupInitialViewController() { 
    showViewController(at: 0) 
}
  • 앱이 실행되었을 때 첫 번째 탭(인덱스 0)을 기본 화면으로 설정합니다.
  • 내부적으로 showViewController(at: 0)을 호출하여 첫 번째 화면을 추가합니다.

showViewController(at:) – 특정 인덱스의 뷰 컨트롤러를 표시

private func showViewController(at index: Int) {
    guard index >= 0, index < viewControllers.count else {
        return
    }

    let viewController = viewControllers[index]
    addChild(viewController)
    view.addSubview(viewController.view)
    viewController.view.snp.makeConstraints {
        $0.top.horizontalEdges.equalToSuperview()
        $0.bottom.equalTo(tabBar.snp.top)
    }
    viewController.didMove(toParent: self)
    
    [gradientView, tabBar, dimmingView, floatingButton, floatingMenu]
        .forEach { view.bringSubviewToFront($0) }
}
  • 특정 인덱스(index)의 뷰 컨트롤러를 현재 화면에 추가합니다.

hideViewController(at:) – 특정 인덱스의 뷰 컨트롤러를 숨김

private func hideViewController(at index: Int) {
    guard index >= 0, index < viewControllers.count else {
        return
    }

    let viewController = viewControllers[index]
    viewController.willMove(toParent: nil)
    viewController.view.removeFromSuperview()
    viewController.removeFromParent()
}
  • 특정 인덱스의 뷰 컨트롤러를 화면에서 제거합니다.
  • willMove(toParent: nil)을 호출하여 뷰 컨트롤러가 사라질 것임을 알림.
  • removeFromSuperview()를 호출하여 화면에서 제거.
  • removeFromParent()를 호출하여 부모 뷰 컨트롤러에서 제거.

© 2024. All rights reserved.

Powered by Hydejack v9.2.1