Design Patterns

느낌대로 만들어본 TabBar, Navigation Coordinator

JoonSwift 2022. 1. 27. 14:44

최근에 Coordinaotr 패턴에 대해 알아보다보니 흥미가 생겨서 좀 더 복잡한 구조에서의 Coordinator 패턴을 어떻게 구성할지에 대해 고민을 해보았습니다. 

많은 앱에서 사용되고, 저의 N행시인 앱에서도 사용중인 UITabBarController와 UINavigationController를 Coordinator 패턴을 활용하여 구성해보는 포스팅이 되겠습니다. 만들고 싶은 프로젝트의 형태는 아래와 같습니다! 

이런 형식의 프로젝트입니다. TabBarController 가 제일 상단에 위치하고, 하나의 탭 마다 UINavigationController들이 존재합니다. 

 

1. SceneDelegate

이 프로젝트에서는 Storyboard를 사용하지 않았습니다. 그렇기 때문에 SceneDelegate 에서 먼저 하나의 흐름이 만들어질 수 있습니다. window에 보여줄 ViewController를 보여주는 흐름입니다. 

SceneRouter

여기서 SceneRouter의 역할은 window를 받아서, 해당 window에 rootViewController를 지정해주고, window의 makeKeyAndVisible()메서드를 호출하는 역할을 줄 수 있습니다. 

class SceneRouter: Router {
  
  private var window: UIWindow
  
  init(window: UIWindow) {
    self.window = window
  }
  
  func present(
    _ viewController: UIViewController,
    animated: Bool,
    onDismissed: (() -> Void)?) {
      window.rootViewController = viewController
      window.makeKeyAndVisible()
  }
  // ...
}

SceneCoordinator

SceneRouter에서 어떤 View Controller를 받아서 어떻게 보여줄지 정의했다면, Coordinator는 그 어떤 View Controller가 무엇인지에 대해 Router에게 알려주어야 합니다. 

이 부분에 저의 많은 고민과 느낌이 들어가있는데, UITabBarController를 하나 생성해주어 router.present를 하여 보여준 다음, 생성한 UITabBarController를 가지는 TabBarRouter와 이 Router를 가지는 Coordinator를 생성하여 SceneCoordinator의 자식으로 생성해 주었습니다. (이렇게 하는게 맞는지 확실하지는 않습니다. ㅠㅠ)

class SceneCoordinator: Coordinator {
  var children: [Coordinator] = []
  var router: Router
  
  // ...
  
  func present(
    animated: Bool,
    onDismissed: (() -> Void)?) {
      let tabBarController = UITabBarController()
      
      self.router.present(
        tabBarController,
        animated: true
      )
      
      let router = TabBarRouter(
        tabBarController: tabBarController
      )
      
      let coordinator = TabBarCoordinator(
        router: router
      )
      
      presentChild(
        coordinator,
        animated: true,
        onDismissed: nil
      ) 
    }
}

여기 까지 하면 우선 Scene에 TabBar가 올려지게 됩니다! 

2. TabBar

TabBar에서는 present가 어떤 역할을 해야할지 고민을 많이했습니다. 탭을 하나하나 누를 때 마다 View Controller들이 나오는 것을 present라고 정의해야하는지? 아니면 TabBar에 각 탭에 보여질 View Controller들을 정의하는 것이 present 일지에 대해 고민하였고, 저는 후자를 택했습니다. 

TabBarRouter

TabBarRouter의 역할은 UITabBarController에 어떤 View Controller들을 보여줄지에 대한 정의가 들어가있습니다. 

import UIKit

class TabBarRouter: NSObject {
  private var tabBarController: UITabBarController
  
  init(tabBarController: UITabBarController) {
    self.tabBarController = tabBarController
    super.init()
  }
}

extension TabBarRouter: Router {
  // ...
  func present(
    _ viewControllers: [UIViewController],
    animated: Bool,
    onDismissed: (() -> Void)?
  ) {
    tabBarController.setViewControllers(
      viewControllers,
      animated: animated
    )
  }
  // ...
}

tabBarController의 setViewControllers 메서드를 활용하여 tabBarController에 들어갈 View Controller들을 설정해주는 present 메서드를 생성해주었습니다.

TabBarCoordinator

그렇다면 Coordinator의 역할은 TabBarRouter에 필요한 View Controller들이 무엇인지, 알려주는 역할을 해야겠습니다. 

import UIKit

class TabBarCoordinator: Coordinator {
  var children: [Coordinator] = []
  var router: Router
  
  init(router: Router) {
    self.router = router
  }
  
  func present(animated: Bool, onDismissed: (() -> Void)?) {
    // UINavigationController 3개 생성하는 코드 생략!
    
    guard let router = router as? TabBarRouter else {
      return
    }
    
    router.present(
      [firstNav, secondNav, thirdNav],
      animated: true,
      onDismissed: nil
    )
    
    // Router, Coordinator 각각 3개 생성하는 코드 생략 !
    
    presentChildren(
      [firstCoordinator, secondCoordinator, thirdCoordinator],
      animated: true,
      onDismissed: nil
    )
  }
  
  private func presentChildren(
    _ children: [Coordinator],
    animated: Bool,
    onDismissed: (() -> Void)?
  ) {
    children.forEach({ child in
      presentChild(
        child,
        animated: true,
        onDismissed: nil
      )
    })
  }
}

여기서 특징적으로 봐야할 점은 또 역시 NavigationController들을 해당 TabBarRouter에게 준 다음, 이를 활용하여 각각의 NavigationRouter를 생성하고, 이 NavigationRouter들은 또 각각의 Coordinator를 생성하는데에 사용되었습니다. 

그리고 모든 Coordinator들은 TabBarCoordinator의 children으로, 마지막에 presentChildren( ... ) 메서드에서 처럼 presentChild를 해준 모습입니다. 

3. Navigation

Navigation은 제일 첫번째인 FirstViewController에 대한 Coordinator만을 확인해보겠습니다.

NavigationRouter

Navigation Router의 역할은 UINavigationController를 활용하여 들어오는 ViewController들을 어떻게 보여줄지 정의하는 역할입니다. 

class NavigationRouter: NSObject {
  private var navigationController: UINavigationController
  
  init(navigationController: UINavigationController) {
    self.navigationController = navigationController
    super.init()
  }
}

extension NavigationRouter: Router {
  func present(_ viewController: UIViewController, animated: Bool, onDismissed: (() -> Void)?) {
    navigationController.pushViewController(
      viewController,
      animated: animated
    )
  }
  // ...
}

present만 구현했습니다.. (하핫)

FirstCoordinator

FirstCoordinator의 역할은 NavigationRouter에서 보여줄 FirstViewController에 대한 생성과, FirstViewController의 버튼 이벤트에 대한 처리를 담당하게 됩니다. 

class FirstCoordinator: Coordinator {
  var children: [Coordinator] = []
  var router: Router
  
  init(router: Router) {
    self.router = router
  }
  
  func present(
    animated: Bool,
    onDismissed: (() -> Void)?) {
      let firstVC = FirstViewController()
      firstVC.delegate = self
      firstVC.title = "First"
      router.present(
        firstVC,
        animated: true
      )
  }
}

extension FirstCoordinator: FirstViewControllerDelegate {
  func didTappedButton(_ viewController: UIViewController) {
    let viewController = PurpleViewController()
    router.present(viewController,
                   animated: true,
                   onDismissed: nil)
  }
}

 

전체적인 구조

 

프로젝트

 

정리..

항상 어떤 View Controller에서 다음 ViewController를 생성하여 push하거나, SceneDelegate에서 TabBarController에 대한 모든 부분을 정의하여 View Controller들를 넣어주다가, Coordinator 패턴을 알고나서, 이런 방식을 사용하다 보니 생각도 조금씩 바뀌게되고, 이런 패턴을 만든다는 것이 정말 대단하다고 생각했다.. 

 

참고자료

A4용지와 볼펜.

'Design Patterns' 카테고리의 다른 글

Design Pattern, RxSwift ) RxFlow 맛보기 - 1  (0) 2022.07.19
Iterator Pattern?!  (0) 2022.02.04
Coordinator Pattern 연습 1  (0) 2022.01.25
Observer Pattern  (0) 2021.06.03