この記事ではCoordinatorパターンを少し変形したパターンを紹介します。
基本のCoordinatorパターンの説明はざっくりです。
詳しく知りたい方は『iOSアプリ設計パターン入門』などをご覧ください。とてもお勧めの書籍です。
基本は大丈夫な場合はスキップ
Coordinatorパターンの基本
CoordinatorパターンはViewControllerをスリムにするパターンです。
複雑になりがちなViewControllerから画面遷移を切り離します。
- ViewControllerで画面遷移をしない。
代わりにdelegateやblockで外(Coordinator)に画面遷移を移譲する。 - CoordinatorはViewControllerを生成する
- Coordinatorは画面遷移をする
最初の画面表示
Listを表示し、選択されたらDetailを表示するアプリを考えます。
protocol Coordinator {
func start()
}
Coordinatorはstart()
があるだけです。
これを呼ぶと画面遷移をします。
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()
}
}
window = UIWindow(frame: UIScreen.main.bounds)
coordinator = ListCoordinator(window: window!)
coordinator!.start()
ListCoordinatorは以下のような動作をします。
- 親を受け取る。
今回はUIWindowを受け取っている。 - 開始する。
ListViewControllerを生成し、UIWindowに表示する。
詳細画面表示
ListViewControllerでタスクが選択されると、ListViewControllerでは画面遷移をせずに以下が呼ばれます。
extension ListCoordinator: ListViewControllerOutput {
func showDetail(taskId: String) {
let coordinator = DetailCoordinator(listViewController: listViewController, taskId: taskId)
coordinator.start()
detailCoordinator = coordinator
}
}
DetailCoordinatorは以下のようになります。
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は以下のような動作をします。
- 親と必要パラーメータを受け取る。
今回はListViewControllerを親とし、追加でTaskIdを受け取ります。 - 開始する。
DetailViewControllerを生成しpresent
する。
詳細画面非表示
DetailViewControllerを非表示にする処理も、DetailViewControllerでは画面遷移せず以下が呼ばれます。
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を用意することになりました。
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で渡すようにします。
これを使うと最初のサンプルコードは以下のように変わります。
class ListCoordinator: Coordinator {
var listViewController: ListViewController!
var detailCoordinator: DetailCoordinator?
init() {}
func start(transition: (UIViewController) -> Void) {
listViewController = ListViewController(output: self)
transition(listViewController)
}
}
window = UIWindow(frame: UIScreen.main.bounds)
coordinator = ListCoordinator()
coordinator!.start {
window!.rootViewController = $0
window!.makeKeyAndVisible()
}
Coordinatorに親の情報を渡す必要はありません。
また画面遷移の詳細はstart()
の呼び出し側に移動しました。
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をクリアする処理も簡単に実装できます。
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を生成させ、特定の位置に挿入するなども書きやすくなると思います。