
Coordinator 패턴은 iOS 애플리케이션의 화면 전환과 화면 흐름을 관리하기 위한 디자인 패턴이다.
일반적으로 UIViewController가 화면 이동과 관련된 로직을 직접 담당하지만, 이 경우 UIViewController가 너무 많은 역할을 맡게 되어 특히 MVC 구조에서 ViewController가 비대해지는 문제가 발생할 수 있다.
이러한 문제를 해결하기 위해 Coordinator 패턴은 화면 전환 로직을 별도의 객체로 분리하여 관리한다.
즉, UIViewController는 화면 UI와 사용자 인터랙션에 집중하고, 화면 흐름과 관련된 로직은 Coordinator라는 객체가 담당하는 방식이다.

protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set } // 자식 코디네이터 보관 -> 강한 참조
var navigationController: UINavigationController { get set }
func start()
}
protocol MainCoordinatorDelegate: AnyObject {
func mainCoordinatorDidRequestDetail(_ coordinator: MainCoordinator, with item: CalculateModel) // Detail 뷰로 push
func mainCoordinatorDidFinish(_ coordinator: MainCoordinator) // 코디네이터 종료 알림
}
Coordinator 패턴을 익히기 위해, 간단히 만들어진 환율 어플에서 환율 계산기 화면으로 이동하는 것을 만들어 보려고 한다.
MainView -> push -> CalculateView -> pop -> MainView
| MainView | CalculateView |
|---|---|
![]() | ![]() |
먼저 전체 Coordinator를 관리할 AppCoorinator를 구현해준다.
// Coordinator Protocol
protocol Coordinator: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
// AppCoordinator
final class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
// 추후 구현
}
}
AppCoordinator는 앱 전체의 화면 흐름을 관리하는 메인 코디네이터이다.
테스트를 위한 앱에서는 MainView와 CalculateView 두 가지의 화면이 있기 때문에, 각 화면별로 하나의 코디네이터를 구현해주어야 한다.
먼저 메인 화면의 코디네이터를 구현해보자
// 메인 화면 코디네이터
final class MainCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = MainViewController()
vc.coordinator = self // 메인 뷰에 weak var coordinator 구현 후 정의
navigationController.viewControllers = [vc]
}
}
// 환율 계산기 화면 코디네이터
final class DetailCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = CalculateViewController()
vc.coordinator = self // 환율 계산기 뷰에 weak var coordinator 구현 후 정의
navigationController.pushViewController(vc, animated: true)
}
}
이렇게 하면 기본적인 구현이 완료되어, 각 코디네이터에서 start() 메서드를 사용하면 화면을 불러올 수 있게된다.
아직 구현이 안된 것이 있는데, 메인 화면에서 계산기 화면으로 넘어가기 위해서는 push 요청을 했다는 것을 AppCoordinator가 알 수 있어야 한다. 이를 위해 Delegate를 구현해 주어야 한다.
// 메인 화면 Delegate
protocol MainCoordinatorDelegate: AnyObject {
func mainCoordinatorDidRequestDetail(_ coordinator: MainCoordinator, with item: CalculateModel) // push 요청
func mainCoordinatorDidFinish(_ coordinator: MainCoordinator) // 코디네이터 명시적 제거
}
// 환율 계산기 화면 Delegate
protocol DetailCoordinatorDelegate: AnyObject {
func detailCoordinatorDidFinish(_ coordinator: DetailCoordinator) // 코디네이터 명시적 제거
}
그리고 이렇게 만들어진 Delegate를 AppCoordinator에 채택한다.
extension AppCoordinator: MainCoordinatorDelegate {
func mainCoordinatorDidRequestDetail(_ coordinator: MainCoordinator, with item: CalculateModel) {
let detailCoordinator = DetailCoordinator(navigationController: navigationController, item: item)
detailCoordinator.delegate = self
childCoordinators.append(detailCoordinator) // 자식 코디네이터를 강하게 참조
detailCoordinator.start()
}
func mainCoordinatorDidFinish(_ coordinator: MainCoordinator) {
childCoordinators = childCoordinators.filter { $0 !== coordinator }
}
}
extension AppCoordinator: DetailCoordinatorDelegate {
func detailCoordinatorDidFinish(_ coordinator: DetailCoordinator) {
childCoordinators = childCoordinators.filter { $0 !== coordinator }
}
}
이제 각 코디네이터에 delegate를 만들어주고, 각 ViewController에서는 상황에 맞게 메서드를 호출하도록 하면 된다.
final class MainCoordinator: Coordinator {
weak var delegate: MainCoordinatorDelegate?
// ...
func showDetail(for item: CalculateModel) {
delegate?.mainCoordinatorDidRequestDetail(self, with: item)
}
func finishMainFlow() {
delegate?.mainCoordinatorDidFinish(self)
}
}
final class DetailCoordinator: Coordinator {
weak var delegate: DetailCoordinatorDelegate?
// ...
func detailCoordinatorDidFinish() {
delegate?.detailCoordinatorDidFinish(self)
}
}
앞서 AppCoordinator에서 구현하지 않은 start() 메서드도 구현해준다.
func start() {
let mainCoordinator = MainCoordinator(navigationController: self.navigationController)
mainCoordinator.delegate = self
childCoordinators.append(mainCoordinator) // 자식 코디네이터를 강하게 참조
mainCoordinator.start()
}
이제 코디네이터를 사용할 준비는 완료되었으니, AppDelegate에서 코디네이터를 사용하여 화면을 불러오면 된다.
AppDelegate에 코디네이터를 구현할 때 중요한 것은, AppCoordinator가 메모리에서 해제되면 안된다는 것이다.
이를 위해 AppCoordinator를 지역 변수가 아닌 프로퍼티로 정의하는 것이 좋다.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator? // 프로퍼티 선언
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
let navigationController = UINavigationController()
window = UIWindow(windowScene: scene)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
appCoordinator = AppCoordinator(navigationController: navigationController)
appCoordinator?.start() // 화면 구현
}
// ...
}

화면의 흐름을 구현할 때, UINavigationController를 사용한다는 것에 큰 의문을 가지지 않았었는데,
Coordinator 패턴을 공부하면서 문제점을 인지하게 되었고, 왜 Coordinator 패턴이 생겼는지 알게 되었다.
앞으로는 앱을 구현할 때 화면 흐름에 대해서도 더 잘 고려를 해봐야겠다고 느꼈다.