Help us understand the problem. What is going on with this article?

iOS meets Flux

More than 3 years have passed since last update.

iOS meets Flux

by kumabook
1 / 42

iOS with Flux

  • Fluxとは
    • SwiftFlux
  • Redux
    • ReSwift
  • with RxExtention
    • Delta

Flux

flux-diagram


Fluxとは


MVCのおさらい

  • Model ... データ構造・ビジネスロジック(笑)
  • View ... Model(データ)を表示
  • Controller ... Viewのイベントを受け取りModelへメッセージを発行

wikipediaより

wiki_mvc


あれっ


一方向じゃね


Image search wth MVC

Screen Shot 2016-06-28 at 11.26.22 PM.png


MVCの問題点

  • シンプルすぎてそれだけでは足りずバリエーションが多い
    • Controller -> View へデータを流しがち
      • サーバサイドMVCの影響
      • アニメーションなどViewが細かい状態をもつとき

MVCの問題点

  • 依存関係が複雑に
    • ViewやModelがどこで変更されたのがわかりづらく


Flux

  • ルールを決めないと気がついたら崩壊しがちなMVCをベストプラクティス的にパターン化したもの
  • 基本はObserverパターン

flux


Structure and Data Flow

flux

  • 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

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を分割

redux


ReSwift


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

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かなどを縛らずに枠組みだけを作れる
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away