버그 잡이

Reactorkit에 coordinator 패턴 적용해보기 본문

IOS

Reactorkit에 coordinator 패턴 적용해보기

버그잡이 2022. 4. 17. 14:58

 

Viper나 RIBS 같은 패턴은 화면 전환 로직을 담당하는 라우터를 모듈화 해서 관리하는 것으로 알고 있는데,

ReactorKit은 화면 전환 로직을 어떻게 관리하는지에 대한 궁금증이 생겼습니다.

 

공식 문서와 샘플 예제를 찾아보니, 라우터를 관리하는 정형화된 방식은 없는 것 같고.

라우터를 모듈화하고 싶으면 ReactorKit에 추가적인 플로우 관련 패턴을 더하는 식으로 진행되는 것 같았습니다.

 

ReactorKit + RxFlow

ReactorKit + Coordinator 패턴

위와 같이 사용되는 것을 볼 수 있었습니다.

 

이번 글에서는 ReactorKit + Coordinator 패턴에 대해서 알아보고자 합니다.

RxFlow는 잠깐 보니 러닝커브도 좀 있고, Coordinator 패턴을 먼저 이해하고 그 다음으로 RxFlow에 도전해보는 것이 좋을 것 같았습니다.

 

 

Coordinator Pattern

 

https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930

 

코디네이터 패턴의 상세한 설명은 제드님 블로그에 잘 나와있으니 먼저 읽어보시는 것을 추천드립니다.

 

코디네이터 패턴에 대해서 저만의 요약을 해보자면

 

1. 코디네이터 패턴을 사용하면 ViewController간의 의존성을 약하게 할 수 있다.

2. 왜냐면 화면 전환시 delegate만 호출하고 실제 구현은 AppCoodinator에서 하기 때문이다.

3. 이를 위해서는 ViewController 마다 Coordinator가 필요하고  delegate를 잘 구현해줘야한다.

 

 

거두 절미하고 예제로 가보겠습니다.

 

예제

 

제가 만든 앱은 다이어리 앱으로 tableView 다이어리 리스트를 보여주고 cell 을 클릭시 다이어리 상세 화면이 뜨는 구조의 앱입니다.

 

코디네이터 패턴 구조는 아래와 같습니다.

 

* AppCoodinator

 

다이어리 앱에서 AppCoordinator은 MainViewController를 띄워주는 역할을 하기 때문에 관련된 로직을 작성해줍니다.

(제드님글의 예시에서 볼 수 있듯이 로그인 여부에 따라서 start() 내부에 로직이 추가될 수 있습니다.)

protocol Coordinator : class {
    var childCoordinators : [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    func start()
}


class AppCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        self.showMainViewController()
    }
    
    func showMainViewController() {
        let coordinator = MainCoordinator(navigationController: self.navigationController)
        coordinator.delegate = self
        coordinator.start()
        self.childCoordinators.append(coordinator)
    }
}

 

 

 

* SceneDelegate

 

앱의 시작 화면을 설정하는 SceneDelegate에서 AppCoodinator를 생성하여 위애서 생성된 로직을 생성해줍니다.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        let window = UIWindow(windowScene: windowScene)
        self.window = window

        let navigationController = UINavigationController()
        self.window?.rootViewController = navigationController
        
        let coordinator = AppCoordinator(navigationController: navigationController)
        coordinator.start()
        
        self.window?.makeKeyAndVisible()
    }
}

 

 

* MainCoodinator

 

AppCoordinator에 의해서 MainCoordinator가 생성되었습니다. 

MainCoordinator를 보면 두 개의 함수가 있습니다.

MainViewController를 실행시키는 로직과 DiaryDetail 화면을 실행시키는 로직입니다.

후자는 정확하게 말하면 DiaryDetailCoodinator를 생성하는 로직입니다.

protocol MainCoordinatorDelegate {
    func showDiaryDetail(_ coordinator: MainCoordinator, diary: Diary)
}

class MainCoordinator: Coordinator, MainViewControllerDelegate {
    var childCoordinators: [Coordinator] = []
    var delegate: MainCoordinatorDelegate?
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        let mainViewReactor = MainReactor()
        let mainVC = MainViewController(reactor: mainViewReactor)
        mainVC.delegate = self
        self.navigationController.viewControllers = [mainVC]
    }
    
    func showDiaryDetail(content: Diary) {
        self.delegate?.showDiaryDetail(self, diary: content)
    }
}

 

"그런데 왜 MainCoordinator에서 DiaryDetailCoordinator를 직접 생성하지 않고 Delegate만 호출할까요?"

 

Coodinator 객체를 여기서 가지고 있지 않기 위함입니다. 이게 Coordinator 패턴의 핵심인데요.

delegate로 호출만 연결하고 구현부를 AppCoodinator에서 관장하게 합니다. 그러면 그 아래의 Coodinator들은 다른 Coodinator를 참조하기 않게 되는 것입니다.

그 결과 의존성이 약해지고 말로만 듣던 의존성 역전이 가능해지는 것 입니다.

아래와 같이 기존의 Coordinator간의 의존성은 줄어들고 AppCoodinator에서 의존성을 관리하게 되는 것이지요.

 

 

* MainViewController

 

tableView의 Cell 선택되었을때, delegate를 호출해줍니다.

MainViewControllerDelegate는 아까 MainCoordinator에서 채택을 해줬죠?

해당 액션을 delegate로 넘겨서 MainCoorinator로 넘기고 MainCoordinator는 이를 AppCoordinator로 넘겨줍니다.

 

그렇다면 AppCoordinator에서 구현을 해보겠습니다.

protocol MainViewControllerDelegate {
    func showDiaryDetail(content: Diary)
}

class MainViewController: UIViewController, View {

    var delegate: MainViewControllerDelegate?

    func bind(reactor: MainReactor) {

		...

        tableView.rx.itemSelected
          .subscribe(onNext: { [weak self] indexPath in
            let content = reactor.currentState.diarySectionList[indexPath.section]
            let diary = content.items[indexPath.row]
            self?.delegate?.showDiaryDetail(content: diary)
          }).disposed(by: disposeBag)
          
        ...
    }
        
}

 

* AppCoordinator (MainCoodinatorDelegate 추가)

 

extension부분을 보면 MainCoodinatorDelegate를 채택해서 구현해준 것을 볼 수 있습니다.

 

class AppCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        self.showMainViewController()
    }
    
    func showMainViewController() {
        let coordinator = MainCoordinator(navigationController: self.navigationController)
        coordinator.delegate = self
        coordinator.start()
        self.childCoordinators.append(coordinator)
    }
}

extension AppCoordinator: MainCoordinatorDelegate {
    func showDiaryDetail(_ coordinator: MainCoordinator, diary: Diary) {
        let coordinator = DiaryDetailCoordinator(navigationController: coordinator.navigationController, diary: diary)
        coordinator.start()
        self.childCoordinators.append(coordinator)
    }
}

 

 

* DiaryDetailCoordinator

class DiaryDetailCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    
    private let diary: Diary
    
    init(navigationController: UINavigationController,
         diary: Diary) {
        self.navigationController = navigationController
        self.diary = diary
    }
    
    func start() {
        let diaryDetailVC = DiaryDetailViewController()
        diaryDetailVC.diary = self.diary
        diaryDetailVC.view.backgroundColor = .white
        self.navigationController.pushViewController(diaryDetailVC, animated: true)
    }
}

 

 

 

"만약 DiaryDetailCoodinator에서 또 새로운 화면(DiaryDetaiEditViewController)을 띄운다면 어떻게 구현해야 할까요?"

 

1. DiaryDetailViewController에서 DiaryDetailEdit 화면 호출 액션을 받아서 delegate로 DiaryDetailCoordinator로 넘긴다
2. DiaryDetailCoodinator는 delegate를 통해서 AppCoodinator에게 구현부를 넘긴다.

3. AppCoodinator에서 DiaryDetailEditCoodinator 호출 로직을 구현한다.

 

 

* DiaryDetailCoodinator

protocol DiaryDetailCoordinatorDelegate {
    func showDiaryDetailEdit(_ coordinator: DiaryDetailCoordinator)
}

class DiaryDetailCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    var delegate: DiaryDetailCoordinatorDelegate?
    
    private let diary: Diary
    
    init(navigationController: UINavigationController,
         diary: Diary) {
        self.navigationController = navigationController
        self.diary = diary
    }
    
    func start() {
        let diaryDetailVC = DiaryDetailViewController()
        diaryDetailVC.diary = self.diary
        diaryDetailVC.view.backgroundColor = .white
        self.navigationController.pushViewController(diaryDetailVC, animated: true)
    }

    func didTapDetailEdit() {
        self.delegate?.showDiaryDetailEdit(self)
    }
}

 

* AppCoodinator

extension AppCoordinator : DiaryDetailCoordinatorDelegate {
    func showDiaryDetailEdit(_ coordinator: DiaryDetailCoordinator) {
        let coordinator = DiaryDetailEditCoordinator(navigationController: coordinator.navigationController)
        coordinator.start()
        self.childCoordinators.append(coordinator)
    }
}

 

이처럼 화면이 추가될 때마다 AppCoodinator에 로직이 하나씩 추가됩니다.

 

 

 

 

 

느낀점

 

* 코디네이터 패턴을 쓰면 화면간의 의존성을 끊을 수 있어서 좋다.

 

* 화면의 재사용성에 있어서도 좋을 것 같다.

 

* 아직은 단순한 화면 Push 구조만 연습해봤는데, 탭바 구조와 네비게이션 스택을 관리할때는 코니네이터 패턴을 어떻게 적용할지 고민해보자

 

 

 

 

반응형
Comments