Coordinator Pattern

ellyheetov·2021년 6월 15일
4
post-thumbnail

Coordinator 패턴의 시작

iOS에서는 화면 전환을 담당하는 컨트롤러인 UINavigationController가 있다.
Stack 방식으로 새로운 화면을 push하고, 이전화면으로 돌아가가기 위해 pop한다. 가장 첫 화면을 기준으로 새로운 화면으로 넘어갈때 마다 순서대로 쌓이고, 뒤로가기 버튼을 통해 이전에 방문했던 화면들을 순서대로 꺼낼 수 있다.

navigationController?.pushviewController(nextViewController, animated:true)

제공되는 push, pop을 이용하여 쉽고 간단하게 화면을 전환 할 수 있지만, 단점이 존재한다.
앱이 점차 커지고 화면이 많아진다면 사용하기가 버거워 진다는 것이다. 화면을 전환하는 코드가 사용하는 view controller를 의존하고, 이 경우 hard-coding 되어있는 것과 다르지 않다. 때문에 관리하기가 점점 힘들어지고 이를 해결하기 위해서 coordinator 패턴을 도입하게 된 것이다.

Coordinator 패턴이란?

view controller로 부터 화면 전환의 부담을 줄여주고, 화면전환을 보다 더 관리하기 쉽도록 도와주기 위한 패턴이다.

coordinator 패턴을 사용하므로써, view controller 사이에 결합도를 낮춰 준다. 각 view controller는 이전에 어떤 컨트롤러가 있었는지, 다음에 어떤 컨트롤러가 오는지 알 필요가 없다. 대신에 이러한 flow는 coordinator가 관리한다. 오르지 coordinator만이 이것을 알고 관리한다.

결과적으로, 어떠한 순서로든 컨트롤러 전환이 가능하고, 재사용 까지도 가능하다. hard-coding을 피할 수 있게 된다.

예시

예를 들자면 다음과 같다. 화면 전환을 위한 coordinator를 만들고, extension하여 화면전환에 필요한 작업을 구현한다.

protocol Coordinator: AnyObject {
  func pushToDetail(_ navigationController: UINavigationController, productId: String)
}

extension Coordinator {

  func pushToDetail(_ navigationController: UINavigationController, productId: String) {
    let vc = DetailViewController()
    vc.setNavigationTitle("상세화면")
    vc.productId = productId
    vc.coordinator = self
    navigationController.pushViewController(vc, animated: true)
  }
}

상세 화면으로 전환해야하는 view controller에 coordinator를 담당하는 delegate를 만들고, 상세 화면으로 전환해야 하는 시점에 coordinator를 통해서 화면을 전환하도록 한다.

class ListViewController: UIViewController {
  weak var coordinator: Coordinator?
  ...
  
  func productTapped(_ productId: String) {
    coordinator?.pushToDetail(navigationController, productId: String)
  }
  
  ...
}

간단하게 coordinator를 사용하는 방법에 대해 살펴 보았다.

Sub-Coordinator

  • ParentCoordinator : childCoordinator를 생성하고 제거하는 역할
  • ChildCoordinator : 화면을 전환하는 역할

앱이 커지는 경우 child coordinator(or subcoordinator)를 사용할 수 있다. 예를 들어 하위 코디네이터를 사용하여 계정 생성 흐름을 제어하고, 다른 하위 코디네이터를 사용하여 제품을 구매하는 흐름을 제어할 수 있다.

하위 코디네이터 흐름을 살펴보자.

간략하게 정리하자면 5가지 단계로 구성된다.

  1. ParentCoordinator 생성 : 모든 코디네이터를 관리할 부모 코디네이터
  2. ChildCoordinator 생성 : 하위 코디네이터
  3. ChildCoordinator의 메소드 호출 : 화면 전환 메소드
  4. ChildCoordinator 제거 메소드 호출: 하위 코디네이터 제거
  5. ParentCoordinator에서 ChildCoordinator 제거

각 흐름을 구체적으로 살펴보자.

예시

Coordinator 프로토콜 정의

먼저 위에서 정의한 coordinator 프로토콜을 조금 수정하고 넘어가자.

protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    
    func start()
}
  1. coordinator는 화면전환시 기준이 되는 네비게이션 컨트롤러를 소유하여, 이를 파라미터로 넘겨받지 않도록 한다.
  2. 부모 자식 관계를 가지는 Coordinator 구조를 설계하므로, childCoordinator 프로퍼티를 추가한다.
  3. 첫 화면을 띄우기 위해서 start() 메소드를 가진다.

ParentCoordinator 정의

그럼 이제 정의한 프로토콜을 채택하는 ParentCoordinator 클래스를 생성한다.

class ParentCoordinator: Coordinator {

    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start(){
        let vc = ViewController.instantiate()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: false)
    }
}

Childcoordinator 정의

class ChildCoordinator: Coordinator {
    // parentCoordinator와 동일
}

그런데, Parent에서 생성된 child는 어떻게 parentCoordinator에 "나 할일 다했어, 이제 지워줘" 메시지를 전달 할 수 있을까?

Parent와 Child사이에 메시지를 주고 받기 위해서 child는 부모를 기억하고 있어야 한다.
때문에 ChildCoordinator는 부모 코디네이터를 프로퍼티로 가진다.

class ChildCoordinator: Coordinator {
    var parentCoordinator: MainCoordinator?
}

여기서 주의 할 점이 있다. 부모 코디네이터는 자식 코디네이터를 알고, 자식 코디네이터가 부모 코디네이터를 알고 있기 때때문에 메모리 참조 순환 문제가 발생한다. 이를 해결하기 위해서는 weak 키워드를 붙여주어야 한다. (자세한 건 여기 참조)

class ChildCoordinator: Coordinator {
    weak var parentCoordinator: MainCoordinator?
}

1. ParentCoordinator 생성

iOS13 버전 이하라면 AppDelegate에서, 이상이라면 SceneDelegate에서 ParentCoordinator를 생성한다.

coordinator = ParentCoordinator(navigationController: navController)
coordinator?.start()

2. ParentCoordinator에서 childCoordinator 생성

class ParentCoordinator: Coordinator {

    //생략
    
    func buySubscription(){
        let child = ChildCoordinator(navigationController: navigationController)
        child.parentCoordinator = self
        childCoordinators.append(child)
        child.start()
    }
}

ChildCoordinator를 생성하고, start 메소드를 호출 하였으므로 화면전환이 정상적으로 수행된다.

3. ChildCoordinator의 화면 전환

class ChildCoordinator: Coordinator {
    func pushToDetail(productId: String) {
    	let vc = DetailViewController.instantiate()
    	vc.setNavigationTitle("상세화면")
    	vc.productId = productId
  	vc.coordinator = self
   	navigationController.pushViewController(vc, animated: true)
     }
}

4. ChildCoordinator 제거 메소드 호출

ChildCoordinator는 일을 다하고, 부모에게 자신이 종료 되었음을 알린다.

func didFinishBuying(){
    parentCoordinator?.childDidFinish(self)
}

5. ParentCoordinator에서 ChildCoordinator 제거

부모 코디네이터네서는 파라미터로 넘어온 자식 코디네이터를 찾아서 제거한다.

func childDidFinish(_ child: Coordinator?){
	for (index, coordinator) in childCoordinators.enumerated() {
		if coordinator === child {
			childCoordinators.remove(at: index)
			break
		}
	}
}

정리

  • Coordinator란 화면전환을 위한 델리게이트이다.
  • 코디네이터는 계층 구조를 가질 수 있다. 부모 Coordinator는 하위 coordinator를 관리한다.

전체코드

gitst - ellyheetov

참고

https://www.hackingwithswift.com/articles/175/advanced-coordinator-pattern-tutorial-ios
http://labs.brandi.co.kr/2020/06/16/kimjh.html
https://khanlou.com/2015/01/the-coordinator/

profile
 iOS Developer 좋아하는 것만 해도 부족한 시간

2개의 댓글

comment-user-thumbnail
2022년 5월 11일

안녕하세요 엘리. 잘 보고가요!

1개의 답글