Coordinatorパターン+αでViewControllerをスリムにする

この記事ではCoordinatorパターンを少し変形したパターンを紹介します。

基本のCoordinatorパターンの説明はざっくりです。
詳しく知りたい方は『[iOSアプリ設計パターン入門](https://peaks.cc/0CDC3575/iOS_architecture)』などをご覧ください。とてもお勧めの書籍です。

基本は大丈夫な場合は[スキップ](https://qiita.com/drafts/80cc67f598589911e08b#coordinatorパターンの拡張)

# Coordinatorパターンの基本

CoordinatorパターンはViewControllerをスリムにするパターンです。
複雑になりがちなViewControllerから画面遷移を切り離します。

* ViewControllerで画面遷移をしない。  
代わりにdelegateやblockで外(Coordinator)に画面遷移を移譲する。
* CoordinatorはViewControllerを生成する
* Coordinatorは画面遷移をする

## 最初の画面表示

Listを表示し、選択されたらDetailを表示するアプリを考えます。

```Coordinator.swift
protocol Coordinator {
    func start()
}
```

Coordinatorは`start()`があるだけです。
これを呼ぶと画面遷移をします。

```ListCoordinator.swift
class ListCoordinator: Coordinator {
    let window: UIWindow
    var listViewController: ListViewController!
    var detailCoordinator: DetailCoordinator?

    init(window: UIWindow) {
        self.window = window
    }

    func start() {
        listViewController = ListViewController(output: self)
        window.rootViewController = listViewController
        window.makeKeyAndVisible()
    }
}
```

```AppDelegate.swift
window = UIWindow(frame: UIScreen.main.bounds)
coordinator = ListCoordinator(window: window!)
coordinator!.start()
```

ListCoordinatorは以下のような動作をします。

1. 親を受け取る。  
今回はUIWindowを受け取っている。
2. 開始する。  
ListViewControllerを生成し、UIWindowに表示する。

## 詳細画面表示

ListViewControllerでタスクが選択されると、ListViewControllerでは画面遷移をせずに以下が呼ばれます。

```ListCoordinator.swift
extension ListCoordinator: ListViewControllerOutput {
    func showDetail(taskId: String) {
        let coordinator = DetailCoordinator(listViewController: listViewController, taskId: taskId)
        coordinator.start()
        detailCoordinator = coordinator
    }
}
```

DetailCoordinatorは以下のようになります。

```DetailCoordinator.swift
class DetailCoordinator: Coordinator {
    let listViewController: ListViewController
    let taskId: String
    var detailViewController: DetailViewController!

    init(listViewController: ListViewController, taskId: String) {
        self.listViewController = listViewController
        self.taskId = taskId
    }

    func start() {
        detailViewController = DetailViewController(output: self, taskId: taskId)
        listViewController.present(detailViewController, animated: true)
    }
}
```

DetailCoordinatorは以下のような動作をします。

1. 親と必要パラーメータを受け取る。  
今回はListViewControllerを親とし、追加でTaskIdを受け取ります。
2. 開始する。  
DetailViewControllerを生成し`present`する。

## 詳細画面非表示

DetailViewControllerを非表示にする処理も、DetailViewControllerでは画面遷移せず以下が呼ばれます。

```DetailCoordinator.swift
extension DetailCoordinator: DetailViewControllerOutput {
    func done() {
        detailViewController.dismiss(animated: true)
        listViewController.refresh()
    }
    func cancel() {
        detailViewController.dismiss(animated: true)
    }
}
```

`dismiss` 処理と、必要な場合は遷移元の`refresh()`を呼び出します。


Coordinatorパターンを使うことで、以下が実現できました。

* ViewControllerは画面遷移の詳細を知らない
* ViewControllerは他のViewControllerを知らない


# Coordinatorパターンの拡張

CoordinatorパターンでViewControllerをスリムにできました。
しかしこのままだと以下の点が気になります。

* ListCoordinatorはDetailViewControllerが非表示になるタイミングがわからない。  
そのためDetailCoordinatorをクリアすることができない（メモリリークする）。
* DetailCoordinatorがListViewControllerを知っている必要はあるだろうか。  
単純なUIViewControllerではいけないか。

上記を考慮した結果、実際のアプリでは以下のProtocolを用意することになりました。

```Coordinator.swift
protocol Coordinator {
    func start(transition: (UIViewController) -> Void)
}

protocol SelfTerminantable {
    associatedtype TerminationState
    typealias TerminationHandler = ((UIViewController, TerminationState) -> Void)
    func terminate(termination: @escaping TerminationHandler)
}
```

画面遷移の詳細は`start()`の中に書かず、`start()`を呼び出した側がClosureで渡すようにします。
また閉じるときの処理もClosureで渡すようにします。

これを使うと最初のサンプルコードは以下のように変わります。

```ListCoordinator.swift
class ListCoordinator: Coordinator {
    var listViewController: ListViewController!
    var detailCoordinator: DetailCoordinator?

    init() {}

    func start(transition: (UIViewController) -> Void) {
        listViewController = ListViewController(output: self)
        transition(listViewController)
    }
}
```

```AppDelegate.swift
window = UIWindow(frame: UIScreen.main.bounds)
coordinator = ListCoordinator()
coordinator!.start {
    window!.rootViewController = $0
    window!.makeKeyAndVisible()
}
```

Coordinatorに親の情報を渡す必要はありません。
また画面遷移の詳細は`start()`の呼び出し側に移動しました。

```ListCoordinator.swift
extension ListCoordinator: ListViewControllerOutput {
    func showDetail(taskId: String) {
        let coordinator = DetailCoordinator(taskId: taskId)
        coordinator.start {
            listViewController.present($0, animated: true)
        }
        coordinator.terminate { (vc, state) in
            vc.dismiss(animated: true)
            if state {
                self.listViewController.refresh()
            }
            self.detailCoordinator = nil
        }
        detailCoordinator = coordinator
    }
}
```

Detailを表示する場合も同様に親の情報を渡す必要はありません。
また画面を閉じたときにListViewControllerを更新する処理はListCoordinatorに移動しました。
ListとDetailに分散していたList関連処理が1箇所にまとまります。

不要になったdetailCoordinatorをクリアする処理も簡単に実装できます。

```DetailCoordinator.swift
class DetailCoordinator: Coordinator, SelfTerminantable {
    typealias TerminationState = Bool

    let taskId: String
    var termination: TerminationHandler!
    var detailViewController: DetailViewController!

    init(taskId: String) {
        self.taskId = taskId
    }

    func start(transition: (UIViewController) -> Void) {
        detailViewController = DetailViewController(output: self, taskId: taskId)
        transition(detailViewController)
    }

    func terminate(termination: @escaping TerminationHandler) {
        self.termination = termination
    }
}

extension DetailCoordinator: DetailViewControllerOutput {
    func done() {
        termination?(detailViewController, true)
    }
    func cancel() {
        termination?(detailViewController, false)
    }
}
```

DetailCoordinatorからListViewControllerを扱う処理がなくなりました。
DetailViewControllerだけ意識するようになります。

# まとめ

CoordinatorパターンはViewControllerから画面遷移を引き剥がします。
ViewControllerが別のViewControllerを生成する必要がなくなり、スリムになります。

また拡張したパターンを使うとCoordinator同士の関連も少し弱まり書きやすくなります。

`present`や`push`ではない独自に`addChild(viewController)`する画面遷移でも、親ViewControllerを渡す必要がありません。
`UITabBarController` に入れるViewControllerを生成させ、特定の位置に挿入するなども書きやすくなると思います。