TL;DR
- 複雑な状態を管理するために、iOSアプリの開発でもFluxアーキテクチャは検討する価値があります。
- SwiftFluxはFluxでのiOSアプリ開発をサポートしてくれます。実際のアプリでの知見も徐々に溜まってきました。
- 今はReduxの良さを取り込めないか試行錯誤中です。フィードバックお待ちしています。
複雑な状態の変化を予測可能にするアーキテクチャ
iOSアプリ開発においてViewControllerのコードは肥大化し続ける傾向があり、複雑な状態をどう管理するかはアプリケーションの複雑度をコントロールすることに直結します。
そんな中で、JavaScriptの世界ではFluxというアーキテクチャが注目を集めています。
FluxにはViewの状態の流れを一方向にするという特徴があり、処理による状態の変化を予測しやすくなる効果があります。
これはiOSアプリの開発においても応用できそうだと考えました。
しかしFluxは考え方にすぎないため、必要な実装は自分でやらないといけません。
そこでSwiftFluxというライブラリを作りました。詳しい情報はREADMEをご覧ください。
SwiftFluxの特徴
JavaScriptにおけるFluxでは、Actionは基本的にはただの文字列で表現され、Store上で文字列比較によってそのActionに反応するかが判断されます。
また、ActionがStoreに渡してくるデータに何が入っているのかはActionの実装を見ないとわかりません。
SwiftFluxではSwiftの型システムを活用してこれを改善しています。
struct CreateTodo : Action {
typealias Payload = Todo
func invoke(dispatcher: Dispatcher) {
let todo = Todo(title: "New ToDo")
dispatcher.dispatch(self, result: Result(value: todo))
}
}
struct TodoStore : Store {
enum TodoEvent {
case Created
}
typealias Event = TodoEvent
let eventEmitter = EventEmitter<TodoStore>()
private var internalTodo: Todo?
var todo: Todo? { return internalTodo }
init() {
ActionCreator.dispatcher.register(CreateTodo.self) { (result) in
swith result {
case .Success(let value):
internalTodo = value
eventEmitter.emit(.Created)
default:
break;
}
}
}
}
Storeが反応するActionは、register
時にActionの型を指定することで自明になります。
更にActionが渡してくる値は、Payload
のtypealias
により宣言的に型が決まります。
そしてStoreが発火するイベントも、StoreのEvent
とEventEmitter
のジェネリクスによって明確になります。
これらによりFluxの各モジュールの接続がコード補完によって強力にサポートされ、流れるように実装することができます。
実際のアプリ開発におけるSwiftFluxのプラクティス
以前Qiitaに書いた時はまだ、僕自身もSwiftFluxを使ってアプリを書いたことはありませんでした。
あれからそこそこの規模のアプリをSwiftFluxで実装した実績もできたところで、いくつかのプラクティスが溜まってきたので共有します。
Actionから複数の型を含むPayloadを送りたい
シンプルなActionならモデルオブジェクトやそのリストなどをPayload
に指定すればいいですが、複数の型を扱いたいときはタプルを使うのが便利です。
例えばある日の予定を更新するようなActionだと、NSDate
とScheduleEvent
の値を送りたくなります。
struct UpdateEvent: Action {
typealias Payload = (NSDate, ScheduleEvent)
let date: NSDate
let scheduleEvent: ScheduleEvent
func invoke(dispatcher: Dispatcher) {
dispatcher.dispatch(self, result: Result(value: (date, scheduleEvent)))
}
}
1つのActionから送る値のパターンが複数ある
Action内の処理の結果によって型が変わる場合はどうすればいいでしょうか?
Actionをそもそも分けてしまえればそれでもよいですが、どうしても1つのActionにしたい場合はenumを使うとよいです。
enum ValueType {
case Date(NSDate)
case Title(String)
case SubItem(Int, Item)
}
struct ChangeValue: Action {
typealias Payload = ValueType
let value: ValueType
func invoke(dispatcher: Dispatcher) {
dispatcher.dispatch(self, result: Result(value: value))
}
}
struct ValueStore: Store {
private var date = NSDate()
private var title = ""
private var subItems = [Item]()
init() {
ActionCreator.dispatcher.register(ChangeValue.self) { (result) in
swith result {
case .Success(let value):
changeValue(value)
eventEmitter.emit(.Changed)
default:
break;
}
}
}
private func changeValue(value: ValueType) {
switch value {
case .Date(let date):
self.date = date
case .Title(let title):
self.title = title
case .SubItem(let index, let item):
self.subItems[index] = item
}
}
}
エラー処理
エラー処理をどこで行うかはFluxでは少し悩むポイントになります。
エラーを状態として扱うべきか、そしてそのハンドリングはActionが行うのかStoreが行うのかは、Fluxでは明確に定まっていません。
個人的には以下のようにenumでViewの状態を列挙し、Error
の引数にErrorType
を持たせる実装がよさそうに感じています。
class TodoStore : Store {
enum ViewState {
case Normal
case Error(ErrorType)
}
private var internalViewState = ViewState.Normal
var viewState: ViewState {
return internalViewState
}
init() {
ActionCreator.dispatcher.register(TodoAction.List.self) { (result) in
switch result {
case .Success(let payload):
...
case .Failure(let error):
self.internalViewState = ViewState.Error(error)
self.eventEmitter.emit(.Changed)
}
}
}
}
class TodoViewController: UIViewController {
let todoStore = TodoStore()
override func viewDidLoad() {
super.viewDidLoad()
todoStore.eventEmitter.listen(.Changed) { () in
switch todoStore.viewState {
case .Error(let error):
showError(error)
default:
renderViews()
}
}
}
}
この場合、例えばshowError
がSVProgressHUDを使って実装されていた場合、HUDがdismissした時点でviewState
を変えるActionを発行するべきかが悩みどころです。
Storeは現在の状態を常に反映するべきですが、それによってまたChanged
イベントが走ってしまうことになります。
もしかしたらエラーのためのストアを別途用意し、エラー表示状態を別で管理するのもありかもしれません。
Storeインスタンスの破棄
StoreインスタンスをViewController毎に生成する場合、それを破棄するタイミングは少し厄介な問題です。
StoreはシングルトンなDispatcherにregister
されているため、単純にViewControllerの破棄しただけではリリースされません。
ViewControllerのdeinit
で、Storeからregister
されたハンドラをunregsiter
する必要があり、パターンとして毎回実装する必要があります。
class TodoStore {
private var dispatchIdentifiers = [String]()
init() {
dispatchIdentifiers.append(
ActionCreator.dispatcher.register(TodoAction.self) { (result) in
...
}
)
}
func unregsiter() {
for identifier in dispatchIdentifiers {
ActionCreator.dispatcher.unregister(identifier)
}
}
}
class TodoViewController {
let store = TodoStore()
deinit {
store.unregister()
}
}
SwiftFluxが提供するStoreBase
というユーティリティクラスがこの実装をテンプレートとして用意しているので、継承すればStoreに毎回この処理を書かなくてよくなります。
ただやはり、そもそも明示的にunregister
しなくてもいいようにしたいと思ってはいて、実装を見直したり、いいアイデアが無いか検討しています。
SwiftFluxの今後
Fluxの実装としては必要な要素はだいたい揃ったので、しばらくは実際のアプリ開発に使うためのよくあるパターンをカバーできるように改善を続けていこうとしています。
最新のアップデートではStoreBase
、ReduceStore
といった、Storeの実装でよくあるパターンを実現したユーティリティクラスを追加しました。
その中で、最近はReduxの思想に注目しています。
Redux
Reduxは考え方はFluxに近いですが、思想としていくつかの制約を設けています。
その中でも Single Source Tree というのは興味深いコンセプトで、
「状態を管理するStoreは1つであるべき。Storeが分散することで状態が追いづらくなるのはよくない」という考え方です。
ReduxではStoreは、Reducer
と呼ばれる「現在の状態とアクションから、新しい状態を返す関数」の登録によって生成され、アプリケーションの状態は全て、その1つのStoreによって管理されます。
アプリケーションの規模が大きくなった場合でも分割されるのはReducerで、Storeは常に1つになります。
そして、ActionはStoreに対して直接dispatch
します。
これにより、Actionの送り先やそれによって実行される状態の変化(Reducer)が明示的になるので、更に状態の変化が追いやすくなります。
また、全ての状態の変化がStore内で管理されているので、Undo/Redoのような処理を容易に実現できます。
SwiftFluxとRedux
SwiftFluxは愚直にFacebook's Fluxを実装しているため、フレームワーク的な制約はあまり存在しません。
チームで開発するなど規模の大きいアプリ開発では、制約によって複雑度をコントロールできる場合があり、そういう観点ではReduxはいい選択肢のように見えます。
逆にSwiftFluxはActionやStoreの実装に決まり事をあまり設けていないので、様々なアプリにおいて柔軟に対応できます。例えばViewのある一部の複雑な状態を持つコンポーネントのみFlux化するということもやりやすいです。
実は一度、Reduxっぽい実装をSwiftFluxのユーティリティとして実験的に取り込もうとしました。
しかし、ReduxはFluxにおけるDispatcherという考え方と相反する思想のため、そのまま導入してもあまり恩恵がなさそうに感じて一旦クローズしています。
Reduxの思想を取り入れるにはもう少しSwiftFluxのベースを活かしたアプローチが求められると思っています。
例えば現在は、複数のStoreを束ねてそれぞれの状態をReduxのように一元的に管理できる StoreGroup
のようなアイデアを考えています。
これも含めていくつかのアイデアを試行錯誤中です。ご意見あればぜひいただきたいです。
まとめ
FluxはViewControllerの責務を細分化し、状態の変化の流れを明示的にしてくれるので、アプリケーションの状態が把握しやすくなります。
SwiftFluxはSwiftのFlux実装では、現実的な選択肢の1つであると思っています。
どこで何が起きたかすぐわかるアーキテクチャ、というのは大規模なアプリでは大きなメリットになるでしょう。
SwiftFluxを試してみた方の感想やご意見、フィードバックをお待ちしております。