Coordinator 패턴 골격 세팅

JuneHyeok Hong·2023년 4월 24일
0

클린 아키텍쳐

목록 보기
2/3

왜 필요할까?

지금까지는 navigation control을 ViewController에서 지협적으로 관리하고 있었다.
사실 navigation control을 관리하고 있다고 보기 힘들었다.

프로젝트 코드를 처음 보는 사람은 뷰들의 흐름을 알기 위해서는 모든 ViewController의 버튼 이벤트 연결 함수를 찾아야 하는 상황이었다.
ViewController의 역할을 생각했을 때 Input, Output의 결과에 따른 뷰의 처리가 주된 역할이기 때문에 ViewController가 navigation 흐름을 관리한다는 것도 역할에 맞지 않는다.

그래서 각 ViewController마다 Coordinator 파일을 만들어 navigation 흐름을 관리하고자 했다.

어떻게 만들었나?

https://github.com/GO-SOPT-iOS-Part/HongJuneHuke/pull/6

1. 먼저 Coordinator Protocal을 만들어 뷰 흐름에 있어서 발생되는 상황에 대비하였다.
예를 들어 뷰가 로드되는 상황, 다른 뷰를 push하는 상황,
다시 pop되는 상황, 루프뷰를 제외하고 모든 뷰가 pop되는 상황을 위한 함수를
Coordinator Protocal에 선언해두었다.

protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    func start()
    func start(childCoordinator: Coordinator)
    func didFinish(childCoordinator: Coordinator)
    func removeChildCoordinators()
}
  1. Coordinator +Extension을 통해 이후 Coordinator Protocal 구현에 쓰일 함수들을 만들어 두었다.
extension Coordinator {
    public func addChildCoordinator(_ childCoordinator: Coordinator) {
        self.childCoordinators.append(childCoordinator)
    }

    public func removeChildCoordinator(_ childCoordinator: Coordinator) {
        self.childCoordinators = self.childCoordinators.filter { $0 !== childCoordinator }
    }
    
    public func removeAllChildCoordinators() {
        childCoordinators.forEach { $0.removeChildCoordinators() }
        childCoordinators.removeAll()
    }
}
  1. 이후 NavigationCoordinator 프로토콜을 만들어 중복으로 사용될 UINavigationController를 선언해두었다.
    여기서 NavigationCoordinator는 Coordinator를 상속받으며 이후 BaseCoordinator가 NavigationCoordinator를 상속 받았을 때 자연스럽게 Coordinator Protocol 또한 상속받도록 만들었다.

  2. BaseCoordinator 클래스에서는 NavigationCoordinator를 상속받으며 Coordinator Protocol에서 선언한 함수들을 구현해준다.

class BaseCoordinator: NavigationCoordinator {
    
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        fatalError("Start method must be implemented")
    }

    func start(childCoordinator: Coordinator) {
        addChildCoordinator(childCoordinator)
        childCoordinator.start()
    }

    func didFinish(childCoordinator: Coordinator) {
        removeChildCoordinator(childCoordinator)
    }
    
    func removeChildCoordinators() {
        removeAllChildCoordinators()
    }
}
  1. AppCoordinator는 SceneDelegate에서 사용되면 첫 navigation control을 관리하게 된다.
    AppCoordinator에서 사용자의 로그인 여부를 확인하여 로그인 뷰를 루트뷰로 할지 홈뷰를 루트뷰를 할지 결정하기도 했다.
class AppCoordinator: Coordinator {

    var childCoordinators: [Coordinator] = []
    private var navigationController: UINavigationController!
    
    var isLoggedIn: Bool = false
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        if self.isLoggedIn {
            self.showMainViewController()
        } else {
            self.showLoginViewController()
        }
    }
    
    private func showMainViewController() {
        //
    }
    
    private func showLoginViewController() {
        // 
    }
}

그래서?

나도 아직 정확히 알고 구현한다기 보다는 여러 레포에서 동작하는 흐름 뜯어가며 만든 Coordinator 기본 골격이다.

확실히 패턴은 구현하는 사람마다 다양한 방법이 있는것 같다.
다른 레포 찾아보니 MVVM과 Rx를 사용할 때는 ViewModel에서 Rx를 통해 버튼 이벤트를 옵저빙하며, ViewModel에서 Coordinator를 사용해 navigation control을 하는 경우가 많았다.

하지만 나는 Rx없이 하고 있어 버튼 이벤트 설정은 viewController에서 진행했다.
viewController에서 버튼 이벤트를 설정하는 로직이 있어도 될지.. 더 좋은 방식이 있을것 같은데.. 아직 고민중이다.

실제로 Rx를 사용하지 않은 레포에서는 Protocol이나 Closure를 사용하여 버튼 이벤트를 ViewModel Input으로 받은 이후 ViewModel에서 Coordinator를 이용해 관리하는것도 있었지만.. 코드길이가 너무 길어지고 navigation 이동만큼 Protocol이나 Closure가 필요할 것 같아 더 좋은 방법은 없는지 찾아보고 있다.

결국... 이러다 Rx로 가는건가 싶긴했다.

0개의 댓글