Coordinator 패턴이란 ViewController가 가지던 화면전환의 책임을 Coordinator 객체가 가지도록 해 기존의 MVC 패턴에서 ViewController가 가지는 많은 책임을 덜어줄 수 있도록 하는 패턴이다.
기존의 화면전환은 다음과 같이 ViewController 내에서 이루어진다.
class ParentViewController: UIViewController {
...
// 특정 버튼을 터치했을 때의 화면전환 및 비즈니스 로직 예시
override func viewDidLoad() {
super.viewDidLoad()
someButton.addTarget(self, action: #selector(touchSomeButton), for: .touchUpInside)
}
@objc func touchSomeButton() {
// ViewController 내에서 이루어지는 화면전환 + 비즈니스 로직
let someData = someFunction()
let childViewController = ChildViewController(someData: someData)
navigationController?.pushViewController(childViewController, animated: true)
}
}
extension ParentViewController: UICollectionViewDelegateFlowLayout {
// UICollectionView를 사용할 때의 화면전환 및 비즈니스 로직 예시
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// ViewController 내에서 이루어지는 화면전환 + 비즈니스 로직
let someElement = someArray[indexPath.row]
let someData = anotherFunction(someElement)
let childViewController = ChildViewController(someData: someData)
navigationController?.pushViewController(childViewController, animated: true)
}
}
다음과 같이 특정 UI 이벤트가 일어날 때, 비즈니스 로직(someFunction(), anotherFunciton(_:))과 화면전환 로직 (pushViewController(_:animated:))이 ViewController에서 동시에 관리되기 때문에 많은 코드가 생성되고 로직이 많아질 수록 유지보수 및 관리가 어렵게 된다.
이러한 문제에서 화면전환은 Coordinator가, 비즈니스 로직은 ViewController가 담당하게 만들어 서로의 역할을 분리해 관리 및 재사용을 유용하게 만드는 것이다.
다음의 화면 계층이 구성되어있다고 가정하자.

앱에 진입하면 StartViewController가 나타나고, 로그인이 안되있을 경우 LoginViewController로, 로그인되어 있을 경우 MainViewController로 화면이 전환된다.
또한 MainViewController에서 회원정보 수정을 하게되면, LoginViewController가 다시 나타나게 된다.
위와 같은 구조를 Coordinator로 구성한다 하면 아래와 같이 변경될 것이다.

우선 모든 Coordinator가 채택하게 될 Coordinator 프로토콜을 구현해주자.
protocol Coordinator: AnyObject {
var childCoordinator: [Coordinator] { get set }
func start()
}
앱의 시작을 관리하는 AppCoordinator를 구현해주고, SceneDelegate에 적용한다.
(예시는 코드 기반의 UI 구현으로 작성되었다.)
// AppCoordinator.swift
import UIKit
//AppCoordinator에서 UINavigationController, UIWindow에 관한 설정을 책임진다.
class AppCoordinator: Coordinator {
var childCoordinator: [Coordinator] = []
private var navigationController: UINavigationController
private let window: UIWindow
init(window: UIWindow) {
let navigationController = UINavigationController()
navigationController.navigationBar.isHidden = true
self.navigationController = navigationController
self.window = window
}
func start() {
window.rootViewController = navigationController
window.makeKeyAndVisible()
}
}
// SceneDelegate.swift
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
guard let window else { return }
appCoordinator = AppCoordinator(window: window)
appCoordinator?.start()
}
}
현재는 어떠한 화면도 나타내지 않는다.
따라서 StartCoordinator를 구현한 다음 AppCoordinator에서 나타낼 수 있도록 구현한다.
(LoginCoordinator, MainCoordinator도 StartCoordinator와 동일한 방식으로 구현한다)
// StartCoordinator.swift
import UIKit
class StartCoordinator: Coordinator {
var childCoordinator: [Coordinator] = []
private var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let startViewController = StartViewController()
navigationController.pushViewController(startViewController, animated: true)
}
}
// AppCoordinator.swift
class AppCoordinator: Coordinator {
...
func start() {
window.rootViewController = navigationController
window.makeKeyAndVisible()
startStartCoordinator()
}
}
extension AppCoordinator {
private func startStartCoordinator() {
let startCoordinator = StartCoordinator(navigationController: navigationController)
startCoordinator.start()
childCoordinator.append(startCoordinator)
}
}
StartViewController에서 '앱 시작하기' 버튼을 누를 시, StartCoordinator에서 로그인을 위해 LoginCoordinator를 시작해야 한다.
따라서 StartViewControllerDelegate를 통해 StartCoordinator에서 버튼 이벤트를 전달받을 수 있도록 하자.
// StartViewController.swift
protocol StartViewControllerDelegate: AnyObject {
func startApp()
}
class StartViewController: UIViewController {
weak var delegate: StartViewControllerDelegate?
private lazy var startButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("앱 시작하기", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(touchStartButton), for: .touchUpInside)
return button
}()
@objc private func touchStartButton() {
delegate?.startApp()
}
...
}
startLoginCoordinator() 메소드의 경우, LoginCoordinator를 시작하도록 구현한다.
// StartCoordinator.swift
class StartCoordinator: Coordinator {
...
func start() {
let startViewController = StartViewController()
startViewController.delegate = self
navigationController.pushViewController(startViewController, animated: true)
}
}
extension StartCoordinator: StartViewControllerDelegate {
func startApp() {
startLoginCoordinator()
}
}
extension StartCoordinator {
private func startLoginCoordinator() {
let loginCoordinator = LoginCoordinator(navigationController: navigationController)
loginCoordinator.start()
childCoordinator.append(loginCoordinator)
}
}
화면 전환 순서를 자세히 정리해보면,
로 구성된다.
여기서 2, 3번을 구현해보자.
우선 LoginViewController에서 '로그인하기' 버튼의 이벤트를 LoginCoordinator로 전달해주어야 한다. 따라서 LoginViewControllerDelegate를 LoginCoordinator가 채택한다.
// LoginViewController.swift
protocol LoginViewControllerDelegate: AnyObject {
func login()
}
class LoginViewController: UIViewController {
weak var delegate: LoginViewControllerDelegate?
private lazy var loginButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("로그인하기", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(touchLoginButton), for: .touchUpInside)
return button
}()
@objc private func touchLoginButton() {
delegate?.login()
}
...
}
하지만 여기서 끝이 아니다.
LoginCoordinator가 LoginViewController에서 로그인 이벤트를 전달받으면 Start 화면으로 돌아가야 하기 때문에, 다시 부모 Coordinator인 StartCoordinator에게 알려야 한다.
따라서 LoginCoordinatorDelegate를 StartCoordinator가 채택해, 로그인이 완료되었다는 이벤트를 StartCoordinator가 전달받도록 구현하자.
그리고 StartCoordinator가 로그인이 완료되었다는 이벤트를 받을 때, MainCoordinator를 start()시켜주면 된다.
// LoginCoordinator.swift
protocol LoginCoordinatorDelegate: AnyObject {
func didLoggedIn(_ coordinator: LoginCoordinator)
}
class LoginCoordinator: Coordinator {
weak var delegate: LoginCoordinatorDelegate?
...
func start() {
let loginViewController = LoginViewController()
loginViewController.delegate = self
navigationController.pushViewController(loginViewController, animated: true)
}
}
extension LoginCoordinator: LoginViewControllerDelegate {
func login() {
// LoginViewController에서 login 메소드가 실행되면, LoginCoordinatorDelegate에서 didLoggedIn 메소드가 실행
delegate?.didLoggedIn(self)
}
}
//StartCoordinator.swift
extension StartCoordinator {
private func startLoginCoordinator() {
let loginCoordinator = LoginCoordinator(navigationController: navigationController)
// LoginCoordinatorDeleagte 채택
loginCoordinator.delegate = self
loginCoordinator.start()
childCoordinator.append(loginCoordinator)
}
private func startMainCoordinator() {
let mainCoordinator = MainCoordinator(navigationController: navigationController)
mainCoordinator.start()
childCoordinator.append(mainCoordinator)
}
}
extension StartCoordinator: LoginCoordinatorDelegate {
// didLoggedIn이 실행되면 LoginCoordinator를 제거, popViewController를 실행한 다음 MainCoordinator를 실행함
func didLoggedIn(_ coordinator: LoginCoordinator) {
self.childCoordinator = self.childCoordinator.filter { $0 !== coordinator }
navigationController.popViewController(animated: true)
startMainCoordinator()
}
}
마지막, Main화면에서 Login화면 전환은 위에서 구현한 방식과 같이 구현하면 된다.
MainViewControllerDelegate를 MainCoordinator가 채택해 프로필설정 버튼 이벤트를 전달받으면, LoginCoordinator를 start()해주면 된다.
//MainViewController.swift
protocol MainViewControllerDelegate: AnyObject {
func profileEdit()
}
class MainViewController: UIViewController {
weak var delegate: MainViewControllerDelegate?
...
@objc private func touchprofileEditButton() {
delegate?.profileEdit()
}
}
//MainCoordinator.swift
class MainCoordinator: Coordinator {
...
func start() {
let mainViewController = MainViewController()
mainViewController.delegate = self
navigationController.pushViewController(mainViewController, animated: true)
}
}
extension MainCoordinator: MainViewControllerDelegate {
func profileEdit() {
startLoginCoordinator()
}
}
extension MainCoordinator {
private func startLoginCoordinator() {
let loginCoordinator = LoginCoordinator(navigationController: navigationController)
loginCoordinator.start()
childCoordinator.append(loginCoordinator)
}
}
구현된 앱은 다음과 같다.!

다음과 같이 Coordiantor 패턴을 도입한 덕에, ViewController에서는 UI 관련 로직과 버튼 이벤트에 대한 로직만 가지게 되었다.
그리고 각 Coordinator에서 화면전환 로직을 갖게 되어 특정 화면으로의 전환을 위한 코드는 모두 Coordinator 내부에서 관리할 수 있게 되었다.
MVC 패턴 뿐만 아니라 MVVM 패턴에서 Coordinator 패턴을 사용하게 되면, View, ViewModel, Coordinator 간의 역할분리가 잘 이루어져 더욱 관리가 수월한 형태가 될 것이다. (코드가 복잡해지고 많아진다면 더더욱)
(전체 예시코드는 하단링크를 참고)
예시코드
https://github.com/JaewoongLee-swift/CoordiantorPatternExample/tree/main
참고
https://khanlou.com/2015/01/the-coordinator/
https://zeddios.medium.com/coordinator-pattern-bf4a1bc46930