FlowControllerの元祖はこちらのIssueで、このIssue提案者が英語で書いた説明記事がこちらです。しかし全て英語でコードが所々しかなく難しかったので、@shizさんの記事をベースにしてつくったサンプルアプリを元に、基本的な『FlowControllerによるpush遷移とpresent遷移のやり方』を解説します。
サンプルアプリ
今回は以下のようなアプリを@shizさんのサンプルコードをベースに作りました。
デモ
画面遷移の流れ
- 記事一覧(ListView)を表示
- push(DetailFirstView)
- push(DetailSecondView)
- push(DetailThirdView)
- present(ModalView)
- push(ModalDetail)
- 完了ボタンで全ての流れを終了
コード
FlowControllerとは何なのか
原文や@shizさんの記事ではCoordinatorパターンと比較する形でFlowControllerのメリットを紹介されています。ただペーペープログラマである僕らにとっては「まずCoordinatorパターンって何?」から入らなければならないため理解に苦しむと思います。
FlowControllerとは、簡単に言うと「通常はViewControllerが担っていた画面遷移の処理(責務)を、ViewControllerから切り分けて代わりに担当してくれるController」です。
具体的には「UIViewControllerを継承したViewControllerのラッパー」であり、構造的にはAppFlowController>HogeFlowController>(NavigationController)>ViewControllerという親子関係になっています。
今までViewControllerで行なっていたpushやpresentの画面遷移を親のHogeFlowControllerに(delegateを使って)委任することで、ViewControllerの責務を減らし、コードを少なくスッキリさせています。
ちょっと何いってるかわかんない
実際のところ、色々な設計パターン(アーキテクチャ)や実装の経験を経てより良い設計に辿り着くのが本来の流れだと思う(知識は必要になった時に初めて身につく)ので、「ちょっと何いっているかわかんない」という人は無理にFlowControllerやCoordinatorパターンを使わなくて良いと思います(←説明の怠慢)。
何が良いのか
こういった設計パターンやアーキテクチャの主目的はどれも同じで「責務の切り分け」とそれによる「テストを容易にすること」です。
このFlowControllerの目的・良さも「ViewControllerの画面遷移の責務を切り分けてやることで、コードの可読性や保守性を良くし、テストも容易にできること」と言えると思います。
Coordinatorパターンでもほぼ同じメリットを得られる(はず)ですが、原文や@shizさんの記事にもあるように「Coordinatorパターンにはないメリットを得られるからFlowControllerを使う」わけで、その詳しい違いについては本記事では割愛します。
追記
僕なりにCoordinatorパターンとFlowControllerパターンの違いをまとめたので最後におまけとして付けておきます)。
今回のサンプルの処理流れ
今回作成したサンプルの構造としては以下の画像のようになっています。
AppFlowController
がすべてのFlowControllerの親です(アプリ起動後にAppDelegate
のapplication(_:didFinishLaunchingWithOptionsAppFlowController)
でAppFlowController
のstart()
を呼んでフローをスタートしています)。
初期画面
最初にListViewController
を表示したいので、まずListFlowController
を生成します(この過程でembeddedNavigationController = UINavigationController()
も生成している)。
次にListFlowController
の中でListViewController
を生成し、embeddedNavigationController.viewControllers = [listViewController]
とすることで初期画面を表示します。
push遷移
そしてNext
ボタンが押された時にdelegate
であるListFlowController
に画面遷移を依頼し、そこで今度はDetailFirstViewController
を生成し、embeddedNavigationController.viewControllers = [firstViewController]
とすることで画面遷移をするわけです。
present遷移
present遷移の場合は新たなナビゲーションを開始(新しいNavigationControllerを生成)しなければならないため、ThirdViewController
からdelegate
であるListFlowController
に依頼し、さらにListFlowController
がdelegate
であるAppFlowController
に新たなフロー(ModalFlowController
)を開始するように依頼します。
ここでpresent遷移をするのはModalFlowController
です。ModalFlowController
もUIViewControllerを継承しているのでpresent遷移が可能で、その子が上下左右ぴたぴたに張り付いているだけなので、見た目としてはModalViewController
がpresent遷移したのと同じになります。
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
children.first?.view.frame = view.bounds
}
flowのremove
最後のFifthViewController
で「完了」ボタンを押した時は、同様にdelegateを使ってNewModalFlowController
を経由して一番親であるAppFlowController
に「フローを終了して!」と依頼する形になります。
フロー開始時にAppFlowController
でadd(childController: listFlowController)
のようにaddしているだけなら、同じくAppFlowController
でself.children.forEach { remove(childController: $0) }
を実行してやれば、今のフローを全て削除してホーム画面などに戻ることができます(サンプルではListFlowを再度startして記事一覧画面を表示しています)。
しかし今回はpresent遷移(下からViewが上がってくる遷移)なのでAppFlowController
でadd(childController: newModalFlowController)
とせずviewController.present(newModalFlowController, animated: true)
としています(ここでのviewControllerはpresent遷移する直前のThirdViewControllerのインスタンスを、delegateで依頼する時に引数としてAppFlowControllerまで渡してきたものです)。
なので画面を消すときもdismiss(上から下に下がる消え方)をしなければなりません。そこでModalFlowController
がAppFlowController
に「フローを終了して」と依頼した直後にself.dismiss(animated: true)
で自分自身をdismissしています。
Coordinatorパターンとの違い(おまけ)
@shizさんの記事の各見出しを僕なりに整理してみました。@shizさんの記事や原文を読んで良くわからなかったときに、僕なりの補足を見ることで皆さんの理解の助けになればと思います(ListFlowControllerのフローの前にもうひとつ、LoginFlowControllerのフローが開始されているので、AppFlowController>LoginFlowController>ViewControllerという構造になっている前提で以下は読んでいただければと)。
3.FlowControllerは依存関係を管理
LoginDependencyContainerなどに依存すべきプロパティ?を追加してやることで簡単に依存性を注入できる?←Coordinatorパターンは?
4. 子FlowControllerの管理が簡単
FlowControllerはUIViewControllerを継承しているので、 addとremoveのメソッド(Extension)だけで子FlowControllerできる。←Coordinatorパターンは?
5.UIWinowを保持する必要がない
Coordinatorパターンの場合はinitで都度UIWindowを触っている。
FlowControllerならUIViewControllerを継承しているので、UIWindowをいじるコードを各必要がない。
6.個々のフローを疎結合にできる
疎結合とはシンプルな結合という意味です(対義語は密結合)。Coordinatorパターンでは親のAppCoordinatorでNavigationControllerを生成して、子であるLoginCoordinatorをinitする時に引数で渡さなければならないから結合度が高い(密結合である)。
FlowControllerなら子であるLoginFlowController自身でNavigationControllerを生成できる。
7. UIResponderを継承している
Coordinatorパターンと違って、FlowControllerはUIViewControllerを継承しているのでtouchesBegan
を始めとした様々なイベントのデリゲートメソッドを使用することができる
。
8. FlowControllerから画面の表示などを管理できる
本来は子VCにさせる表示関係の仕事も、親であるFlowControllerも一応できるよってことではないかと思います(でもそれをしてしまうと役割切り分けた意味なくね?臨時策? )。
9. NavigationControllerの戻るボタン
CoordinatorパターンはNavigationの「戻る」ボタンを手動で設定しなければならない。
FlowControllerは(UIViewControllerを継承しているので?)自動でやってくれる。
10. コールバックで画面遷移を依頼する
よく分からない。Coordinatorパターンがどうやって画面遷移をしているか知らない(おい)。
FlowControllerは「ViewControllerが、delegate(親であるLoginFlowController)に依頼して画面遷移する」のですが、コールバックは使ってないんじゃないのかな…(画面遷移完了したらViewControllerでバック後の処理を実行するとかにはなっていないと思うのだが)。
まとめ
まだ記事を書いてる途中で理解がうやむやだったりするので、リプいただければ随時補足・修正していきます!