JosephCha의 개발일지

Coordinator 패턴 본문

iOS

Coordinator 패턴

JosephCha 2023. 8. 9. 17:58
반응형

ViewController에서의 화면전환 문제점은?

1. ViewController에서 화면 전환은 아래 코드처럼 간단히 구현할 수 있음

self.navigationController?.present(anotherViewController, animated: true)

2. 하지만 앱이 화면이 많아지고 복잡해짐에 따라, 동일한 화면으로 화면 전환하고 싶은 곳이 많아지거나 기존 같은 화면이 아닌 다른 화면으로 전환하고 싶게 된다면, 해당 코드들을 다 찾으면서 수정하는 번거로운 일이 발생함.

 

3. 이러한 화면 전환 코드는 작성된 ViewController가 담당하게 되고, 하드코딩되어 있어 관리하기 힘들어짐 (안그래도 많은 일을 담당하는데..)

 

4. View Controller간의 의존성도 생기게 됨

Coordinator 패턴이란?

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

 

2. Coordinator 패턴을 사용함으로써, ViewController 사이에 결합도를 낮춰줌. 각 ViewController는 이전에 어떤 컨트롤러가 있었는지, 다음에 어떤 컨트롤러가 오는지 알 필요가 없음. 대신에 이러한 flow는 Coordinator가 관리함. 오로지 Coordinator만이 이것을 알고 관리함

 

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

 

4. ViewController은 데이터를 표시하는 법 외에는 아무것도 모르게됨. 따라서 재사용하기 용이해짐

 

5. 앱의 모든 작업과 하위 작업은 전용 캡슐화 방법을 제공

 

6. Coordinator는 display-binding을 side effects와 분리.

ViewController를 표시할 때 ViewController가 데이터를 엉망으로 만들지 여부에 대해 걱정할 필요가 없어짐. 읽기 및 디스플레이만 할 수 있으며 데이터를 쓰거나 손상시키지 않음.

 

7. Coordinator는 완전히 제어할 수 있는 객체

viewDidLoad가 호출 될 때 까지 기다리지 않고 전적으로 show를 제어할 수 있음. 호출을 받는 대신 호출을 시작함

기본 예시!

- MainCoordinator.swift

protocol Coordinator: AnyObject {
    func start()
    func pushDetailViewController(name: String)
}

class MainCoordinator: Coordinator {
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        let mainVC = MainViewController()
        mainVC.coordinator = self
        navigationController.pushViewController(mainVC, animated: true)
    }
    
    func pushDetailViewController(name: String) {
        let detailVC = DetailViewController()
        detailVC.name = name
        detailVC.navigationItem.title = "상세화면"
        navigationController.pushViewController(detailVC, animated: false)
    }
}

- MainViewController.swift

final class MainViewController: UIViewController {
    weak var coordinator: Coordinator?
    
    func tapButton() {
        coordinator?.pushDetailViewController(name: "Joseph Cha")
    }
}

- DetailViewController.swift

final class DetailViewController: UIViewController {
    var name: String?
}

- SceneDelegate.swift

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var coordinator: Coordinator?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }
        
        let navigationController = UINavigationController()
        
        self.window = UIWindow(windowScene: windowScene)
        self.window?.rootViewController = navigationController
        self.window?.makeKeyAndVisible()
        
        self.coordinator = MainCoordinator(navigationController: navigationController)
        self.coordinator?.start()
    }
}
  • MainVC가 첫화면일 때, 해당 화면전환을 담당할 MainCoordinator를 생성함
  • Appdelegate or SceneDelegate에서 MainCoordinator의 start 메소드를 통해 첫화면을 띄움
  • MainVC에서 버튼을 눌러(tapButton) DetailVC로 이동하고자, MainVC에 coordinator를 담당하는 delegate를 만들고tapButton 메소드 호출 시 해당 delegate를 통해 MainCoordinator에서 DetailVC로 화면 전환 하도록 함

Sub-Coordinator

ParentCoordinator : childCoordinator를 생성하고 제거하는 역할

ChildCoordinator : 화면을 전환하는 역할

 

앱이 커지는 경우 Child coordinator(or subcoordinator)를 사용할 수 있다. 예를 들어 ChildCoordinator로 계정 생성 흐름을 제어하고, 다른 ChildCoordinator로 제품을 구매하는 흐름을 제어할 수 있다.

 

또한 ViewController들의 화면전환을 관리하는 Coordinator가 하나일 경우, 어떤 ViewController는 자신과 연관없는 다른 ViewController의 화면전환에 대한 메소드 정보를 Coordinator Delegate 사용 시 불필요하게 알게 될 수 있다. 하나의 Coordinator를 ChildCoordinator들로 쪼개면, 각각의 ViewController들은 자신과 연관된 화면 전환만 알게 될 수 있다.

 

하위 코디네이터 흐름은 간략히 다음과 같다.

ParentCoordinator 생성 ->  ChildCoordinator 생성 -> ChildCoordinator 화면 전환 -> ChildCoordinator 제거

예시

Coordinator 프로토콜 정의

protocol Coordinator {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }
    
    func start()
    func finish()
}
  1. Coordinator는 화면전환 시 기준이 되는 네비게이션 컨트롤러를 소유함
  2. 부모 자식 관계를 가지는 Coordinator 구조를 설계하므로, childCoordinator 프로퍼티를 추가함
  3. 해당 Coordinator의 화면 흐름 시작을 위한 start 메소드 추가함
  4. ViewController(or ViewModel)에서 해당 Coordinator 화면흐름을 끝내기 위한 finish메소드도 생성

ParentCoordinator 생성

final class ParentCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController
    
    init(_ navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        let vc = ViewController()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: true)
    }
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    var appCoordinator: Coordinator?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        let navigationController = UINavigationController()
        
        self.window = UIWindow(windowScene: windowScene)
        self.window?.rootViewController = navigationController
        self.window?.makeKeyAndVisible()
        
        self.appCoordinator = ParentCoordinator(navigationController)
        self.appCoordinator?.start()
    }
}
  1. Coordinator 프로토콜을 채택한 ParentCoordinator 생성
  2. Appdelegate나 SceneDelegate에서 ParentCoorinator생성 및 start메소드 호출

ChildCoordinator 정의

final class ChildCoordinator: Coordinator {
    weak var finishDelegate: CoordinatorFinishDelegate?
    // 생략
}

protocol CoordinatorFinishDelegate: AnyObject {
    func coordinatorDidFinish(childCoordinator: Coordinator)
}
  1. ChildCoordinator가 할일이 마무리 되어 ParentCoordinator에게 자신을 지워 달라고 요청하기 위한 finishDelegate도 포함해야함

ChildCoordinator 생성

class ParentCoordinator: Coordinator {
    //생략
    func showChild(){
        let child = ChildCoordinator(navigationController: navigationController)
        child.finishDelegate = self
        childCoordinators.append(child)
        child.start()
    }
}
  1. ParentCoordinator에서 ChildCoordinator 생성 및 ChildCoordinator 화면 흐름 시작

ChildCoordinator의 화면전환 예시

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

final class ChildViewController: UIViewController {
    weak var coordinator: Coordinator?
    
    func tapButton() {
        coordinator?.pushToDetail(name: "Joseph Cha") // childCoordinator를 통한 화면전환
    }
}
  1. start메소드를 통한 화면 흐름 시작 이후, ViewController(or ViewModel)에서 ChildCoordinator를 통해 화면전환

ChildCoordinator제거

final class ChildCoordinator: Coordinator {
    weak var finishDelegate: CoordinatorFinishDelegate?
    
    func finish() {
        finishDelegate?.coordinatorDidFinish(childCoordinator: self)
    }
    // 생략
}

extension ParentCoordinator: CoordinatorFinishDelegate {
    func coordinatorDidFinish(childCoordinator: Coordinator) {
        for (index, coordinator) in childCoordinators.enumerated() {
            if coordinator === childCoordinator {
                childCoordinators.remove(at: index)
                break
            }
        }
    }
}
  1. ChildCoordinator가 해당 화면 종료를 원할 시, ParentCoordinator에서 자기 자신을 지우도록 finishDelegate의 제거 메소드를 호출함

참고

https://velog.io/@ellyheetov/Coordinator-Pattern

https://duwjdtn11.tistory.com/644

https://labs.brandi.co.kr/2020/06/16/kimjh.html

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

'iOS' 카테고리의 다른 글

동시성 프로그래밍(개념, GCD, OperationQueue)  (0) 2023.08.16
RxSwift? 이거 하나로 종결 (전체 요약)  (2) 2023.08.09
iOS MVVM 패턴  (0) 2023.08.09
SOLID 원칙  (0) 2023.08.09
네트워크 Endpoint  (0) 2023.08.09
Comments