Coordinator: 主要概念、iOSナビゲーションのためのコーディネーターパターン

著者: IT Sectr 公開日: 2026-02-18 読了時間: 9 分

Coordinator(コーディネーター)— 画面間の遷移ロジックをViewControllerから個別のクラスに移すアーキテクチャパターンです。このパターンは2015年にSoroush Khanlouによって提案され、iOSコミュニティで広く採用されました。Coordinatorはアプリケーションのフローを管理します。ViewControllerの作成と表示、画面間のデータ受け渡し、フロー完了の処理を行います。このパターンはコントローラーからナビゲーションを抽出することでMassive View Controller問題を解決します。詳細はCoordinatorの原著記事をご覧ください。

重要ポイント

  • Coordinator — ViewControllerから遷移ロジックを抽出するナビゲーションパターン
  • 責任の分離 — ViewControllerがUIを管理し、Coordinatorがナビゲーションを管理
  • Router — UINavigationControllerを抽象化するCoordinatorの補助コンポーネント
  • Flow — 1つのCoordinatorが管理する画面の順序(例:オンボーディング)
  • 委譲 — 子コーディネーターがdelegate/protocolを介して親に報告

Coordinatorとは:ナビゲーションパターンの本質

Coordinator — iOSアプリケーションのナビゲーション責任を担うパターンです。標準のUIKitでは、ViewController自身が遷移を管理します。present、push、show segue — すべてのナビゲーションメソッドはUIViewControllerから呼び出されます。Coordinatorはこのロジックを抽出します。ViewControllerがイベントを報告し(例:「ユーザーがログインボタンをクリック」)、Coordinatorが次に表示する画面を決定します。ViewControllerはUIロジックのみを保持し、ナビゲーションをコーディネーターに委譲します。

パターン構造 — start()メソッドとfinish()メソッドを持つCoordinatorProtocol。start() — フローの開始:最初のViewControllerの作成と表示。finish() — 親コーディネーターへの通知を伴うフローの完了。Router — UINavigationController(またはUISplitViewController)のラッパーで、show、push、pop、dismissメソッドを提供します。コーディネーターはRouterを介してのみ動作し、UINavigationControllerを直接操作しません。これによりナビゲーションのテストが可能になり、UIフレームワークの切り替えも容易になります。

コンポーネント役割
Coordinatorナビゲーションフローの管理AuthCoordinator, ProfileCoordinator
RouterUINavigationControllerの抽象化push, present, pop, dismiss
ViewControllerUI + Coordinatorへのイベント委譲LoginViewController.delegate

Coordinatorが解決する問題 — Massive View Controller(ナビゲーションはコントローラー肥大化の一般的な原因)。標準のUIKitでは、ViewControllerにprepareForSegue、ナビゲーションデリゲート、unwind segue処理が含まれます。Coordinatorはこれらを排除します。StoryboardのSegueは画面間の静的な接続ですが、Coordinatorは条件付きの動的ナビゲーションを提供します。Router呼び出しの順序を検証することで、UIなしでCoordinatorをテストできます。

SwiftでのCoordinator:RouterとFlowによる実装

Swiftの基本Coordinator — Router用の関連型とstart/finishメソッドを持つプロトコル。Router — UINavigationControllerを抽象化するプロトコル。Routerの具象実装はUINavigationControllerをラップし、メソッドを委譲します。CoordinatorはinitでRouterを受け取り、ナビゲーションに使用します。子コーディネーターはライフサイクル管理のためにchildCoordinators配列に格納されます。

swift
// Router — ナビゲーションの抽象化
protocol RouterProtocol: AnyObject {
    func push(_ viewController: UIViewController, animated: Bool)
    func pop(animated: Bool)
    func present(_ viewController: UIViewController, animated: Bool)
    func dismiss(animated: Bool)
}

final class NavigationRouter: RouterProtocol {
    private let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func push(_ vc: UIViewController, animated: Bool) {
        navigationController.pushViewController(vc, animated: animated)
    }

    func pop(animated: Bool) {
        navigationController.popViewController(animated: animated)
    }

    func present(_ vc: UIViewController, animated: Bool) {
        navigationController.present(vc, animated: animated)
    }

    func dismiss(animated: Bool) {
        navigationController.dismiss(animated: animated)
    }
}

// Coordinator — フロー管理
protocol CoordinatorProtocol: AnyObject {
    var childCoordinators: [CoordinatorProtocol] { get set }
    var router: RouterProtocol { get }
    func start()
    func finish()
}

class AuthCoordinator: CoordinatorProtocol {
    var childCoordinators: [CoordinatorProtocol] = []
    let router: RouterProtocol

    init(router: RouterProtocol) {
        self.router = router
    }

    func start() {
        let loginVC = LoginViewController()
        loginVC.onLogin = { [weak self] in
            self?.showHome()
        }
        router.push(loginVC, animated: true)
    }

    private func showHome() {
        let homeCoordinator = HomeCoordinator(router: router)
        childCoordinators.append(homeCoordinator)
        homeCoordinator.start()
    }

    func finish() {
        childCoordinators.removeAll()
        router.pop(animated: true)
    }
}

AppDelegate/SceneDelegateでのCoordinator作成 — AppDelegateまたはSceneDelegateがUINavigationControllerを作成し、NavigationRouterでラップし、ルートCoordinator(AppCoordinator)を作成してstart()を呼び出します。AppCoordinatorはアプリケーションの状態に応じて、オンボーディング、ログイン、メイン画面のいずれを表示するかを決定します。Coordinatorはナビゲーションの唯一のエントリポイントであり、ViewControllerは他の画面について知りません。

子Coordinatorとコーディネーター階層

コーディネーター階層 — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator。子コーディネーターは親によって作成され、childCoordinators配列に格納されます。子コーディネーターが作業を完了すると、親のfinish()を呼び出し、親はchildCoordinatorsから削除します。これによりメモリリークを防止します。CoordinatorはRouterを介してViewControllerへの強い参照を持つため、childCoordinatorsから削除しないとオブジェクトは解放されません。

swift
// Coordinator -> Parent通信用Delegate
protocol AuthCoordinatorDelegate: AnyObject {
    func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}

class AuthCoordinator: CoordinatorProtocol {
    weak var delegate: AuthCoordinatorDelegate?

    func finish() {
        delegate?.authCoordinatorFinished(self)
    }
}

// AppCoordinator — 親
class AppCoordinator: AuthCoordinatorDelegate {
    func startAuthFlow() {
        let authCoordinator = AuthCoordinator(router: router)
        authCoordinator.delegate = self
        childCoordinators.append(authCoordinator)
        authCoordinator.start()
    }

    func authCoordinatorFinished(_ coordinator: AuthCoordinator) {
        childCoordinators.removeAll { $0 is AuthCoordinator }
        startMainFlow()
    }
}

childCoordinatorsの管理 — Coordinatorを配列から削除することが解放する唯一の方法です。完了したCoordinatorを削除し忘れると、ViewControllerとともにメモリに残り続けます。推奨アプローチ:親のdidMove(toParent:)、完了時のcallback、またはCombine publisherによる自動削除。Coordinatorパターンは通知メカニズムを指定しません — delegate、closure、Combineのいずれかで、開発者の選択に委ねられます。

コーディネーター間のデータ受け渡し方法

delegateによるデータ受け渡し — 子Coordinatorがdelegateプロトコルを定義し、そのメソッドを通じて結果を渡します:func authCoordinator(_:didLoginWith user: User)。親がプロトコルを実装し、子フロー完了時にデータを受け取ります。型安全で明示的です。欠点:子Coordinatorごとに個別のプロトコルが必要。10以上のCoordinatorがあるプロジェクトではファイル数が増加します。

Result型によるデータ受け渡し — finishメソッドはResultを受け取り、Outputはフロー結果のジェネリック型です。Coordinator — 関連する結果型を持つジェネリック。callback付きのstart():start(completion: @escaping (Output) -> Void)。これによりコードが削減され、各コーディネーター用の個別プロトコルが不要になります。RxSwift/Combine:CoordinatorはPassthroughSubject/Publisherを介して結果を公開します。選択はチームのアーキテクチャアプローチに依存します。

方法利点欠点
Delegate型安全、明示的、個別プロトコルプロトコル多数、大量のボイラープレート
Closureコンパクト、ファイル数削減retain cycleのデバッグが困難
Combine/Rxリアクティブ、組み合わせ容易ライブラリ依存、デバッグが複雑

共有データ層 — Coordinatorはデータを直接渡さず、共有サービス/リポジトリを使用します。AuthCoordinatorはトークンをKeychain/UserDefaultsに保存し、ProfileCoordinatorはそこから読み取ります。Coordinatorは直接呼び出しではなく、共有状態(Dependency Injectionコンテナ)を介して通信します。これによりCoordinator間の結合度は低下しますが、共有状態への暗黙的な依存関係が生じます。

CoordinatorとRouter、VIPER、MVVM-Cの比較

Coordinator vs Router — RouterはCoordinatorのコンポーネントでUINavigationControllerを抽象化します。Coordinatorはフロー(どの画面を表示するか)に責任を持ち、Routerはメカニクス(どのように表示するか:push/present)に責任を持ちます。Routerは「方法」、Coordinatorは「何を」です。RouterはCoordinatorなしで使用できますが(例:Navigatorシングルトン)、CoordinatorをRouterなしで使用すると、単に異なる抽象化を持つViewControllerになります。通常は両方のパターンを一緒に使用します。

Coordinator vs VIPER — VIPERにはナビゲーションを担当するWireframeコンポーネントがあり、Coordinatorに類似しています。VIPERではWireframeはモジュールの一部ですが、Coordinatorはモジュール上の個別の層です。VIPERモジュール(View-Interactor-Presenter-Entity-Router)はナビゲーションをモジュールの一部として含みます。Coordinatorはモジュールの外部にあり、モジュールを作成して接続しますが、その一部ではありません。Coordinatorは異なるフローでの画面再利用においてより柔軟です。

MVVM-C — Coordinatorを使用したMVVMの拡張。ViewModelはCoordinatorを直接知りません — ViewControllerがViewModelを介してナビゲーションを委譲し、ViewModelがプロトコルを介してcoordinatorを呼び出します。MVVM-CはSwiftUIを用いたiOSプロジェクトの標準的アプローチです。CoordinatorはNavigationStackまたはfullScreenCoverを管理し、ViewModelは状態を公開することでcoordinatorを呼び出します。AppleはSwiftUIでのCoordinatorを推奨していません — NavigationStackとNavigationPathは組み込みのナビゲーションメカニズムです。

swift
// MVVM-C: ViewModelがプロトコル経由でCoordinatorを呼び出す
protocol AuthNavigationProtocol: AnyObject {
    func showMainScreen()
    func showForgotPassword()
}

class AuthViewModel: ObservableObject {
    weak var navigation: AuthNavigationProtocol?

    func loginTapped() {
        // ロジック...
        navigation?.showMainScreen()
    }
}

よくある質問

SwiftUIにCoordinatorは必要ですか?

SwiftUIでは、組み込みのナビゲーション(NavigationStack、NavigationPath)がCoordinatorを置き換えることがよくあります。Appleはpath-basedナビゲーションを推奨しています。Coordinatorは複雑な条件分岐を伴うフロー(ロールに応じたオンボーディング-ログイン-メイン画面)に意味があります。シンプルなSwiftUIアプリケーションではCoordinatorは過剰です — NavigationPathを使用してください。

CoordinatorはRouterですか?

いいえ、これらは異なるパターンです。Coordinatorはナビゲーションフローを管理します:どの画面を表示するかを決定し、ViewControllerを作成して接続します。RouterはUINavigationControllerの抽象化です:push、present、pop、dismiss。CoordinatorはRouterを使用してナビゲーションを実行します。一部の実装ではRouterがCoordinatorのロジックを含みますが(Router-per-screen)、これは元のパターンからの逸脱です。

Coordinatorでretain cycleを回避するには?

リークの主な2つの原因:childCoordinators(親が子を保持したまま削除を忘れる)とRouter(UINavigationControllerがViewControllerを保持)。解決策:finish()時に子Coordinatorを必ず配列から削除。delegateにはweak参照を使用。Routerについては、UINavigationControllerがすでにwindow階層にある場合は強参照を保持しない。Coordinatorのdeinitをテストしてください。

Coordinatorが過剰なのはどのような場合?

3〜5画面のアプリケーションではCoordinatorは過剰です — segueまたは単純なnavigationController.pushViewControllerで十分です。NavigationStackを使用するSwiftUIアプリケーションでも同様に過剰です。Coordinatorは15画面以上、複雑なフロー(分岐のあるオンボーディング、パスワードリカバリのある認証)、およびUIKit/SwiftUI混合プロジェクトで正当化されます。

Coordinatorをテストするには?

Mock Router — どのメソッドがどのパラメータで呼び出されるかを確認。childCoordinatorsの確認:start()後に配列が空でないこと、finish()後に空であること。CoordinatorはUIなしでテスト可能:Routerはプロトコルであり、そのモックはUIKitを必要としません。非同期フローにはXCTestExpectationを使用。Androidでも同様に、モックナビゲーションを使用したNavigationControllerとNavHostのテストが行われます。

まとめ

  • Coordinator — ViewControllerから遷移を個別クラスに抽出するナビゲーションパターン
  • Router — Coordinatorが使用するUINavigationControllerの抽象化
  • 階層 — 結果委譲による親子Coordinator構造
  • MVVM-C — Coordinatorを使用したUIKitプロジェクトの標準的アプローチ
  • SwiftUI — NavigationStackによる組み込みナビゲーションがCoordinatorを置換
  • テスト — UIKitなしでmock Routerを通じてCoordinatorをテスト

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください