はじめに
こちらはiOS その2 Advent Calendar 2016 6日目の記事です。
今年は仕事でiOSを触る機会がめっきりと減ってしまったのですが、
「やはり1年を振り返るのならiOS Advent Calendarは欠かせないでしょう」ということで投稿することにしました。
最近、筆者が仕事で着手し始めた Redux に関連するということで ReSwift について見ていきたいと思います。
タイトルが (2) になっているのは、
以前に興味を持って自主的に取り組んでみたReSwiftを勉強してみよう!(1)の続きという意味です。
しかし、Advent Calendarの記事でもあるので本記事のみで完結する形で書きたいと思います。
今回は公式GitHubに上がっているCounterExample-Navigation-TimeTravelを元にReSwiftを勉強していきたいと思います。
ReSwiftに出てくるモノと役割
ReSwiftに出てくるモノと役割について改めて見直しをしてみましょう。
- Store
- アプリ内で必ず1つの存在
- アプリの状態を管理する
- Stateを更新するための
dispatch
を提供する- 言い換えれば
dispatch(action)
をすることでStoreにStateの変更を知らせられる
- 言い換えれば
- Stateの状態を追えるように
subscribe
を提供する- 言い換えれば
subscribe(listener)
をすることでlistenerはgetState
を通してStateの状態を取得できる
- 言い換えれば
サンプルでは下記が該当します。
Storeの宣言は下記のようになります。
1 2 3 4 5 6 7 |
|
このサンプルではReSwiftRecorderモジュールを利用しているため通常のStore宣言とは異なります。
ですが、重要なのは、reducer
にAppReducer()
を指定していることと、Storeが管理するStateの初期値をstate: nil
としているということです。
- State
- アプリの状態
サンプルでは下記の通りです。
1 2 3 4 5 |
|
サンプルではCounterViewController
でカウント数の増加および減少をさせる処理が実装されており、
tabBarController
やButton
のアクションなどで表示される画面を切り替える処理が実装されているため上記のようにcounter
とnavigationState
の2つでアプリの状態を表すと定義しています。
- Action
- 何をするアクションなのかを表すオブジェクト
type
プロパティを必ず持つ
- ActionCreator
- Actionを作成するメソッド
サンプルでは下記の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
サンプルのCounterAction.swift
のCounterActionIncrease
はActionおよびActionCreatorの役割を兼ねています。
- Reducer
- ActionとStateから新たなStateを作成して返す
- ポイントはStateを更新するのではなく、 新しく作成したState を返すということ
サンプルでは下記の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
サンプルでは、handleAction
メソッドで「引数として受け取ったaction
とstate
から作成したAppState
を返却」しています。
カウントアップの処理がReducerに伝わった場合には、
counterReducer
メソッドでそのActionに合わせて新たに必要なStateの情報を生成しています。
(カウントアップの場合はCounterActionIncrease
アクションなのでcounter += 1
しています。)
ReSwiftに出てくるモノ同士の連携
1個1個のモノと役割については理解が進みました。
続いて、それらの連携について見ていきましょう。
そのためにはViewController
を見ていくのがわかりやすいかなと思います。
カウントアップ・ダウンに見る連携
まずはCounterViewController
ですが、
「+」や「-」ボタンをタップすることで画面中央に表示されたカウントを増減させる画面です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
細かく見ていきましょう。
Storeの役割で説明した「Stateの状態を追えるようにsubscribe
を提供する」に相当する部分が、
1 2 3 4 5 6 7 |
|
になります。
viewWillAppear
でsubscribe
を実行することで、その画面が表示されるときにStateの監視を開始して、viewWillDisappear
でunsubscribe
を実行することで、その画面が非表示になるときにStateの監視を終了することにしています。
また、「Stateを更新するためのdispatch
を提供する」に相当する部分が、
1 2 3 4 5 6 7 8 9 10 11 |
|
になります。
ユーザが「+」ボタンをタップしたときにincreaseButtonTapped
が呼び出されます。
そのときに上記アクションが実行されたことをdispatch
を実行することでStoreに知らせています。
decreaseButtonTapped
も同様です。
画面切り替えに見る連携
続いてtabBarController
による画面の切り替えですが、これもこのサンプルでは状態変化として扱っています。
ただし、ReSwiftRouter
モジュールで役割を担っているので、これを利用すれば開発者が新たに書く部分は非常に少なくなります。
UITabBarControllerDelegate
を利用してタブをタップしたタイミングをキャッチします。
そのときに、ReSwiftRouter
のSetRouteAction
を利用してタブによる画面切り替えのアクションをStoreに伝えています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
因みに、SetRouteAction
は下記のように定義されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
中身を見てみるとCounterAction
と同様にtoStandardAction
を利用しているのがわかります。
デバッグしていくとわかりますが、tabBarController
を通した画面の切り替えでは、カウントアップやカウントダウンは無関係であるためAppReducer
ではcountの状態を増減させることはありません。
変化するのはAppState.navigationState
のみです。
こちらも追っていくとわかるのですが、下記のNavigationReducer
に処理が引き継がれています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
このように、NavigationReducer
の中でNavigationState
型のオブジェクトを生成して返しています。
なので、アプリの状態であるAppState
が持つのは、
- counter: アプリのカウント状態
- navigationState: アプリの画面状態
と言えます。
まとめ
相変わらず、(流行っていないというのもあるかもしれませんが…)ReSwiftに関する日本語の記事が少ないですね。
恐らく、Web業界のようにFluxやReduxが実際のサービスに取り入れられているとは言い難いのでしょう。
その理由として考えられるのは、iOSの場合はデフォルトとしてStoryboardやViewControllerというものが存在し、Appleもそれをそのまま利用することを推奨しているからかもしれません。
しかしながら、複雑なアプリが世の中で求められるようになるに従って『ViewControllerの肥大化』や『複数人での開発による統一性の崩れ』といった課題により真剣に考えざるを得なくなってきました
。
必ずしも、これらの課題を解決するためにReSwiftを使わなければならないということはないのですが、1つの方法論として知っておくことで選択肢も増えてきます。
ただ、WebでReact&Reduxを取り入れるのと同様に理解するためのハードルが高くもあるので現場で嫌がられることもあるかもしれません。
また、保守性として高いとは決して言えません。開発メンバーの入れ替えが発生したときにReSwiftを勉強するコストが発生します。
Webと違ってそこまで浸透しているとは言えないため、より導入が難しいと言えるでしょう。
ここにある種の矛盾が生じているわけですね…
筆者がちょうどReact&Reduxに触れる機会が増えてきたため、良い機会だと思って、iOSでのReSwiftの利用メリットなどを合わせて考えていきたいと思っています。
そろそろ個人でのアプリ開発も復活させたい気持ちもありますし、その際には積極的に導入してみようかな…
と言ったところで本日はここまで。