画面遷移の悩みを解決するApplicationCoordinatorの実例を紹介してみる

iOSアプリを実装している中で、「画面遷移周りがどうしても密結合になって辛いな・・・」と悩んだことがある人は結構多いのではないかと思います。
よくあるものだと、

* URL SchemeやDeep Linkにて、安定して画面を開けるようにしたい。
* push遷移での表示を前提にしたViewControllerになっていてModalで出せない。
	* navigationController?.push等を直呼び出ししている。
* 複数画面にまたぐフロー(登録フォームとか)の管理が複数のViewControllerに分散してる・・・

ApplicationCoordinatorにて、その大部分を解決できそうなので紹介してみようと思います。
説明のために、サンプルプロジェクトを作ってみました。
https://github.com/yuutetu/CoordinatorSample/tree/master

<img src="https://qiita-image-store.s3.amazonaws.com/0/12413/34dbeb57-9b65-fe57-5368-99b8d02826bb.gif" width=50%>

## ApplicationCoordinator

ApplicationCoordinatorは、対象の画面を遷移方法・閉じる方法・他の画面への遷移方法も含めて管理するレイヤーを設ける方法です。
例えば、画面遷移するときにはCoordinatorクラスをこんな感じで使うイメージになります。

```swift
// Coordinatorを使っていないとき
navigationController?.pushViewController(ItemDetailsViewController.viewController(), animated: true)

// Coordinatorを使っているとき
let itemDetailsCoordinator = ItemDetailsCoordinator(source: navigationController)
itemDetailsCoordinator.start()
```

ApplicationCoordinatorは、2016年のtry! Swiftの https://speakerdeck.com/ayanonagon/shi-jian-de-boundaries にて提案されたのが発端です。
ApplicationCoordinatorについてはよくまとめられてある記事も多く、特に https://qiita.com/endorno@github/items/d0b25c47e3c5a7b48865 は非常に分かりやすいです。
また、実際に使用したサンプルコードも https://github.com/AndreyPanov/ApplicationCoordinator が非常に参考になります。

今回は、具体例を幾つかピックアップしてどう嬉しいのか・どう運用していけるのかをより掘り下げてみたいと思います。

## 具体例

### どの画面にいても安定して指定の画面を開けるようにする

ApplicationCoordinatorを使うと、ApplicationCoordinatorを辿っていくことにより安全にpushするためのNavigationControllerにたどり着くことができます。

[リポジトリ上でのApplicationCoordinatorクラス](https://github.com/yuutetu/CoordinatorSample/blob/master/sample-coordinator/Coordinators/ApplicationCoordinator.swift)では、ログイン画面とメイン画面の切り替えを管理しています。
この実装を例に、どう辿っていくのかを見ていきます。

#### イニシャライザにて一通りのCoordinatorを揃えておく

`init()`では、表示しうるCoordinatorを予め生成して揃えておきます。
手元のvariableに保持しておき、辿れるようにしておきます。

```swift
init(window: UIWindow) {
    self.window = window
    loginCoordinator = LoginCoordinator(window: window)
    mainCoordinator = MainCoordinator(window: window)
    
    // ログイン状態変更を受取
    NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "LoginManagerLoginStateChanged"), object: nil, queue: nil) { _ in
        self.updateUsingCoordinator()
    }
}
```

#### 状況によってCoordinatorを切り替え

表示の切り替えはこのメソッドを呼ぶことによってかんたんに切り替えられます。
それぞれの`start()`を呼ぶことによって画面が切り替わります。

```swift
private func updateUsingCoordinator() {
    if LoginManager.shared.isLoggedIn {
        state = .main
        mainCoordinator.start()
    } else {
        state = .login
        loginCoordinator.start()
    }
}
```

#### URL Schemeで画面遷移したいときの処理

どのCoordinatorを使用しているかを知り、そのCoordinatorをしっかり保持しているため、容易に子のCoordinatorに画面表示の橋渡しをすることができます。

```swift
// 下記のコードでFlowCoordinatorでの画面遷移を開始する。
// (UIApplication.shared.delegate as? AppDelegate)?.applicationCoordinator?.startFlow()
// 実際にはURLSchemeを引数に与えて遷移する実装をするといいかもしれない。
func startFlow() {
    switch state {
    case .some(.main):
        mainCoordinator.startFlow()
    default:
        return // Main Interfaceのときのみ開ける
    }
}
```

### 画面のpush, popの方法を管理する

ApplicationCoordinatorは画面構造に対して画面遷移する方法を責務に持っているため、他の画面への遷移やpop, popToRootの方法もApplicationCoordinatorが指定した方がいいです。

具体的には、ViewControllerからTransitionDelegateの形でどの画面に遷移したいかを受取り、ApplicationCoordinatorが画面遷移の処理をします。
これにより、ViewController側は「navigationControllerがあるか？」を気にせず画面構造と切り離すことができます。
サンプル: https://github.com/yuutetu/CoordinatorSample/blob/master/sample-coordinator/Coordinators/NavigationCoordinator.swift

### 複数画面にまたがる遷移フローを管理する

ApplicationCoordinatorは、画面遷移の複数の連続にも対応できます。
複数入力フォームにまたがる送信画面・確認画面はよくあるUIであると思います。

[サンプル](https://github.com/yuutetu/CoordinatorSample/blob/master/sample-coordinator/Coordinators/FlowCoordinator.swift)では、3連続のpushの画面遷移の後に一覧の画面をpopする処理を実装しています。
フローの開始時の状態として`navigationController.viewControllers`を一旦保持し、フロー終了時にセットし直すことによってフロー前の状態に戻しています。
また、Coordinatorが他の画面への遷移方法も管理しているため、「フロー中に予想していない画面に遷移する」ようなことを防ぐことができます。


## まとめ

ViewControllerにおいて、最も密結合になりやすい画面遷移周りはApplicationCoordinatorによって改善できました。
アーキテクチャを問わず使える軽量でかつ柔軟性も高い概念です。
Nextとして、画面遷移アニメーションもうまいことApplicationCoordinatorで管理できないか考えてみたいと思います。