LoginSignup
20
16

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-06

この記事では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を生成させ、特定の位置に挿入するなども書きやすくなると思います。

20
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
16