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

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

基本のCoordinatorパターンの説明はざっくりです。

詳しく知りたい方は『iOSアプリ設計パターン入門』などをご覧ください。とてもお勧めの書籍です。

基本は大丈夫な場合はスキップ


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同士の関連も少し弱まり書きやすくなります。

presentpushではない独自にaddChild(viewController)する画面遷移でも、親ViewControllerを渡す必要がありません。

UITabBarController に入れるViewControllerを生成させ、特定の位置に挿入するなども書きやすくなると思います。