iOS with Flux
- Fluxとは
- SwiftFlux
- Redux
- ReSwift
- with RxExtention
- Delta
Flux
Fluxとは
- Application Architecture のひとつ
- 2014/5/6 にF8でトーク
- https://facebook.github.io/flux/docs/overview.html
- yet another MVC
- 一方向のデータフローが特徴
MVCのおさらい
- Model ... データ構造・ビジネスロジック(笑)
- View ... Model(データ)を表示
- Controller ... Viewのイベントを受け取りModelへメッセージを発行
wikipediaより
あれっ
一方向じゃね
Image search wth MVC
MVCの問題点
- シンプルすぎてそれだけでは足りずバリエーションが多い
- Controller -> View へデータを流しがち
- サーバサイドMVCの影響
- アニメーションなどViewが細かい状態をもつとき
- Controller -> View へデータを流しがち
MVCの問題点
- 依存関係が複雑に
- ViewやModelがどこで変更されたのがわかりづらく
Flux
- ルールを決めないと気がついたら崩壊しがちなMVCをベストプラクティス的にパターン化したもの
- 基本はObserverパターン
Structure and Data Flow
- Action
- Dispatcher
- Store
- View
A Single Dispatcher
- データフローの中心
- Storeを更新するメッセージ(Action)をここに向けて発行する
- Dispatcherが実際にActionを実行
- 依存関係解決の仕組みもある: Dispatcher.waitFor(dispatherId, () => ())
Store
- アプリの状態とロジック
- Modelと似ているが、複数のオブジェクトの状態を扱う
- 単にコレクションを扱うということではない
- インタフェースとしてはシングルトン
Views and Controller-Views
- Storeのstateやデータを画面に表示
- front endではReactを使う
- StoreからまるっとデータをとってきてViewを構築すれば、差分を見つけて描画する
- iOSではここを少し工夫しないといけないかも(後述)
Actions and Action Creators
- Dispatcherが公開しているメッセージ+ペイロード
- アプリにとって意味のあるイベントという感じ
- Commandパターンみたいなもの - Actionを作るためにActionCreatorというヘルパーメソッドを作ることもある
- ViewのUIイベントをActionCreatorを使ってActionに変換して、Dispatcherに投げる
iOS開発
- 手持ちの武器
- UIKit
- ViewController + View
- Delegate pattern
- DataSource
- NSNotificationCenter
- Key-value observing
- UIKit
iOS開発あるある
- ViewControllerに直接データ保存して、後からViewController間でデータを共有したくなって辛い
- Viewから直接データ更新
- NSNotificationCenterのキーが増えてきて辛い
- 誰が発行したイベントかわからない
Let's flux in swift
- SwiftFlux
- protocol やベースクラスで骨組みを提供
Action
public protocol Action {
typealias Payload
typealias Error: ErrorType = NSError
func invoke(dispatcher: Dispatcher)
}
Dispatcher
public protocol Dispatcher {
func dispatch<T: Action>(action: T, result: Result<T.Payload, T.Error>)
func register<T: Action>(type: T.Type, handler: (Result<T.Payload, T.Error>) -> ()) -> DispatchToken
func unregister(dispatchToken: DispatchToken)
func waitFor<T: Action>(dispatchTokens: [DispatchToken], type: T.Type, result: Result<T.Payload, T.Error>)
}
Store
extension Store {
....
public func subscribe(handler: () -> ()) -> StoreListenerToken {
return eventEmitter.subscribe(self, handler: handler)
}
public func unsubscribe(listenerToken: StoreListenerToken) {
eventEmitter.unsubscribe(self, listenerToken: listenerToken)
}
public func unsubscribeAll() {
eventEmitter.unsubscribe(self)
}
public func emitChange() {
eventEmitter.emitChange(self)
}
}
Example
class TodoListViewController: UITableViewController {
let todoStore = TodoStore()
override func viewDidLoad() {
super.viewDidLoad()
self.todoStore.subscribe { () in
self.tableView.reloadData()
}
ActionCreator.invoke(TodoAction.Fetch())
}
@IBAction func createTodo() {
ActionCreator.invoke(TodoAction.Create(title: "New ToDo"))
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.todoStore.todos.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("TodoCell") as UITableViewCell!
cell.textLabel!.text = self.todoStore.todos[indexPath.row].title
return cell
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
ActionCreator.invoke(TodoAction.Delete(index: indexPath.row))
}
}
SwiftFlux所感
- NSNotificationCenterよりは100倍いい
- Storeを変更するものはそれぞれActionを定義するので、コードが明示的、説明的になる
- コード量は増える、周りくどいと感じるかも
- ViewはStoreの値から毎回作り直すのが基本なので、パフォーマンスが気になる
- 差分更新をViewControllerやCustom Viewで頑張るか
- StoreからのViewへの変更通知は基本変更したかどうかだけだが
- Storeからのイベントで差分更新できるペイロードを渡すか
- Viewが状態も持つのやっぱり辛い
Redux
- Fluxの派生版
Can Redux be considered a Flux implementation?
Yes, and no.
- Fluxは状態の管理が規定されてなかったので、状態の管理にフォーカスしている
Single source of truth
- アプリケーションの状態をオブジェクトのツリーを持つStoreで表現(Single state tree)
- サーバの状態もstateに変換、落とし込むので、ユニバーサルアプリケーションがつくりやすい
- stateが一つだから、デバッグしやすい、開発しやすい
- Single State treeならUndo/Redoも簡単
- time travel
- やっぱりCommandパターンっぽい
State is read-only
- 状態の変更はActionを発行するしかない
- ビューやコールバックが状態を直接変更させることはできない
- すべての変更は中央管理され変更は一つずつ順番に行なわれる
- actionはplain objectsなので、保存可能であり、テストしやすい
Changes are made with pure functions
- アクションがどのように状態を変更するかを「Reducer」で行う。
- Reducer: (prevState, Action) -> (nextState)
- Reduce: Array#reduce((prev, current) -> (next), initialValue)
- Stateはimmutable. 現在のstateを変更せずに、新しいstateを作って返すというのがポイント。
- 最初はアプリケーションで一つのReducerから初めて巨大化してきたらReducerを分割
ReSwift
- ReSwift/ReSwift
- Redux in Swift
- ReSwift/CounterExample-Navigation-TimeTravel
- ReSwiftRouter, ReSwiftRecoder でtime travel
Reducer
public protocol Reducer : AnyReducer {
associatedtype ReducerStateType
public func handleAction(action: Action, state: Self.ReducerStateType?) -> Self.ReducerStateType
}
- Stateがassociatedtypeで型パラメータ: ReducerStateType
- ActionとState受け取ってStateを返す関数
AppReducer
struct AppReducer: Reducer {
func handleAction(action: Action, state: AppState?) -> AppState {
return AppState(
counter: counterReducer(action, counter: state?.counter),
navigationState: NavigationReducer.handleAction(action, state: state?.navigationState)
)
}
}
counterReducer
func counterReducer(action: Action, counter: Int?) -> Int {
var counter = counter ?? 0
switch action {
case _ as CounterActionIncrease:
counter += 1
case _ as CounterActionDecrease:
counter -= 1
default:
break
}
return counter
}
Store
- グローバルに一個もつ
AppDelegate.swift
var mainStore = RecordingMainStore<AppState>(
reducer: AppReducer(),
state: nil,
typeMaps:[counterActionTypeMap, ReSwiftRouter.typeMap],
recording: "recording.json"
)
StoreSubscriver
StoreSubscriber
public protocol StoreSubscriber : AnyStoreSubscriber {
associatedtype StoreSubscriberStateType
public func newState(state: Self.StoreSubscriberStateType)
}
- ViewControllerなどでconfirmする
ViewController
class CounterViewController: UIViewController, StoreSubscriber, Routable {
@IBOutlet var counterLabel: UILabel!
override func viewWillAppear(animated: Bool) {
mainStore.subscribe(self)
}
override func viewWillDisappear(animated: Bool) {
mainStore.unsubscribe(self)
}
func newState(state: AppState) {
counterLabel.text = "\(state.counter)"
}
@IBAction func increaseButtonTapped(sender: UIButton) {
mainStore.dispatch(
CounterActionIncrease()
)
}
....
}
Delta
- thoughtbot/Delta
- 状態管理だけにフォーカス
- RxExtensionと組み合わせて使う
- observerパターンの実装をRxに任せている
- ReactiveCocoa: MutableProperty
- observerパターンの実装をRxに任せている
ActionType
public protocol ActionType {
typealias StateValueType
func reduce(state: StateValueType) -> StateValueType
}
- reducer っぽい
Property
public protocol StoreType {
typealias ObservableState: ObservablePropertyType
var state: ObservableState { get set }
mutating func dispatch<Action: ActionType where Action.StateValueType == ObservableState.ValueType>(action: Action)
func dispatch<DynamicAction: DynamicActionType>(action: DynamicAction) -> DynamicAction.ResponseType
}
- この ObservableStateをReactiveCocoa.MutablePropertyになるようにstructを実装している
- protocol extensionでうまい事実装してるっぽい
まとめ
- Flux
- MVC比べて見通し良さげ
- コード量は増えるかも
- SwiftFluxなど
- Redux
- 状態管理をさらに見通しよく
- グローバル変数で集中管理なので、好き嫌いあるかも
- 一個だけに決まっているのならむしろ乱立しない分良いかもしれない
- Delta
- Rxを使ってるならこれがいいかも
- Rxのユーティリティが使えるのが魅力
雑多な感想
- Swiftに型フレンドリーなObserverユーティリティがないのでみんな自前実装している
- Multiple Delgates欲しい
- ObserverパターンやCommandパターンなど個々の要素は既存パターン
- クラインアントアプリに向けに特化して、組み合わせにベストプラクティスを提供してくれている
- protocolで実現されてるとcool
- classかstructかなどを縛らずに枠組みだけを作れる