LoginSignup
81
74

More than 5 years have passed since last update.

SwiftFluxで複雑な状態の変化を予測可能にするiOSアプリ開発

Last updated at Posted at 2015-12-03

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が渡してくる値は、Payloadtypealiasにより宣言的に型が決まります。
そしてStoreが発火するイベントも、StoreのEventEventEmitterのジェネリクスによって明確になります。
これらによりFluxの各モジュールの接続がコード補完によって強力にサポートされ、流れるように実装することができます。

実際のアプリ開発におけるSwiftFluxのプラクティス

以前Qiitaに書いた時はまだ、僕自身もSwiftFluxを使ってアプリを書いたことはありませんでした。
あれからそこそこの規模のアプリをSwiftFluxで実装した実績もできたところで、いくつかのプラクティスが溜まってきたので共有します。

Actionから複数の型を含むPayloadを送りたい

シンプルなActionならモデルオブジェクトやそのリストなどをPayloadに指定すればいいですが、複数の型を扱いたいときはタプルを使うのが便利です。
例えばある日の予定を更新するようなActionだと、NSDateScheduleEventの値を送りたくなります。

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()
          }
       }
   }
}

この場合、例えばshowErrorSVProgressHUDを使って実装されていた場合、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の実装としては必要な要素はだいたい揃ったので、しばらくは実際のアプリ開発に使うためのよくあるパターンをカバーできるように改善を続けていこうとしています。
最新のアップデートではStoreBaseReduceStoreといった、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を試してみた方の感想やご意見、フィードバックをお待ちしております。

81
74
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
81
74