ReSwiftのサンプルコード程度であれば、RxSwiftの機能を使って自前で実装ができてしまうのでは、と考えたので実際にやってみました。不必要に依存ライブラリを増やしたくないという気持ちがモチベーションとなっています。まずはReSwiftのサンプルコード的なものを確認するところから始めます。
ちなみにこの記事は ReSwift
を使い始めて数時間の人間が書いているので、間違っている可能性が大いにあります。嘘を嘘と見抜きながら読んでいただけると幸いです(そして、コメント欄で刺してください)。
ReSwiftとは?
ReduxのSwift実装らしいです。そもそもReduxがよくわからないのですが、そんなものわかってなくても動くコードはかけそうでした。ということで、とりあえずコードを書いて読んでから、小難しい記事を読んで理解してゆく方針にして、Redux および ReSwift の解説は他の記事に譲ります(手抜き)。
簡単な数字のカウントアップ・アプリケーション
まずはReSwiftを用いてどのようにアプリケーションを構成していくか、ごくごく簡単な数字のカウントアップ・アプリケーションの例を用いてみていきましょう(ただし実際にはこんな単純すぎるアプリケーションに小難しいアーキテクチャを採用するメリットはないと思っています、あくまで例示としてのお話です)。まず CocoaPods
でも Carthage
でもなんでもいいので適当に ReSwift
を導入してください。
ReSwiftではアプリケーションの持つ状態をStateTypeプロトコルを持つ構造体 or クラスにまとめるようです。
struct AppState: StateType {
var counter: Int = 0
}
つづいて Action
の宣言です。今回のアプリケーションに発生しうるアクションとして数字をカウントアップするアクションとカウントダウンするアクションを宣言してみます。
struct CountActionIncrease: Action {}
struct CountActionDecrease: Action {}
簡単ですね?さて続いては、アクションを解釈して、新しい状態を発行する役割を持つ Reducer
を実装してみましょう。状態を変化させるのではなく 新しい状態を発行する
という点に注意しながらコードをみてみると良いと思います。
struct CounterReducer: Reducer {
typealias ReducerStateType = AppState
func handleAction(action: Action, state: ReducerStateType?) -> ReducerStateType {
switch action {
case _ as CountActionIncrease: return AppState(counter: state.map { $0.counter + 1 } ?? 0)
case _ as CountActionDecrease: return AppState(counter: state.map { $0.counter - 1 } ?? 0)
default: return AppState(counter: 0)
}
}
}
そんなに難しいものではなく、action
がもし、CountActionIncrese
型だったら、現在の状態のcounterをカウントアップしたものを生成して返すというだけです。Decrease
も同様に考えれば同じようなものですね。
最後に皆さんおなじみの ViewController
を実装してみましょう。まずは、ストーリーボードに適当にラベルとボタンを配置してください。
つづいて、ViewController
に label
と ボタンのアクションを紐づけて、以下のようなコードを記述します。
import UIKit
import ReSwift
class ViewController: UIViewController, StoreSubscriber {
private let store: Store<AppState> = Store<AppState>(reducer: CounterReducer(), state: AppState(counter: 0))
@IBOutlet weak var label: UILabel!
@IBAction func increaseButtonOnClick(sender: UIButton) {
store.dispatch(CountActionIncrease())
}
@IBAction func decreaseButtonOnClick(sender: AnyObject) {
store.dispatch(CountActionDecrease())
}
override func viewDidLoad() {
super.viewDidLoad()
store.subscribe(self)
}
func newState(state: AppState) {
label.text = String(state.counter)
}
}
各アクションで文字通り Action
を発行しているだけで、ラベルのテキストを書き換えたりカウンターの状態(数字)を書き換えたりしていない点が非常に重要なポイントです。アプリケーションの状態の書き換えは、Reducerで行います。そして、ビューの書き換えは newState
で行います。こうすることにより、状態遷移(実際には新しいステートの発行)の記述と、ビューへの反映を分離しています。
以上のような簡単なアプリケーションだとしても、多分コードみただけじゃ、感覚はつかめないのではないでしょうか?簡単なのでXcode起動して、ちゃちゃっと実験してみてください。5分で終わるので絶対自分の手でコードを書いてみてください。
RxSwiftを使って、上述の形式のコードを実現する
細かいことは抜きにして、RxSwiftを使って同じ動きをエミュレートしてみましょう。まずは state, action, reducer の定義から。
struct AppState {
var counter: Int = 0
}
protocol Action {}
struct CountActionIncrease: Action {}
struct CountActionDecrease: Action {}
struct CounterReducer {
typealias ReducerStateType = AppState
func handleAction(action: Action, state: ReducerStateType?) -> ReducerStateType {
switch action {
case _ as CountActionIncrease: return AppState(counter: state.map { $0.counter + 1 } ?? 0)
case _ as CountActionDecrease: return AppState(counter: state.map { $0.counter - 1 } ?? 0)
default: return AppState(counter: 0)
}
}
}
ほとんど変化はないですね?次に ViewController
は次のような感じ。ちょっと記述は長くなりましたが、だいたい読み替えは可能なのではないでしょうか?
import UIKit
import RxSwift
class ViewController: UIViewController {
private var disposeBag: DisposeBag = DisposeBag()
private let store: (PublishSubject<Action> ,Variable<AppState>) = (PublishSubject<Action>(), Variable(AppState(counter: 0)))
@IBOutlet weak var label: UILabel!
@IBAction func increaseButtonOnClick(sender: UIButton) {
store.0.onNext(CountActionIncrease())
}
@IBAction func decreaseButtonOnClick(sender: AnyObject) {
store.0.onNext(CountActionDecrease())
}
override func viewDidLoad() {
super.viewDidLoad()
store.0
.map { action in CounterReducer().handleAction(action, state: self.store.1.value) }
.doOn(onNext: newState)
.bindTo(self.store.1)
.addDisposableTo(disposeBag)
}
func newState(state: AppState) {
label.text = String(state.counter)
}
}
もちろん、厳密なお話をするとおなじものにはなっていないのですが、ちゃんとプロトコルと基底となるコードをちょっと書いてあげれば、たちまち同じようなものになるのではないでしょうか?(自分の理解が間違っていたらご指摘をお願いします:ReSwiftを触ってまだ1日目です)
おわりに
楽なので、よくやってしまいがちなのは、ViewControllerに var
で状態を宣言して、イベント処理時に変化させていくという感じの実装ではないでしょうか?このサンプルレベルの簡単なアプリケーションであれば、その実装は正しいと思います。シンプルでわかりやすいので、別にそれで全然かまわないと思います。
しかし状態の数が増えると、結果としてアプリケーションの取りうる状態の数は、各プロパティの取りうる状態の組み合わせ(高校で習った数学的なお話)になります。状態が10個程度を超えてくると、もうアプリケーションコードを読むのが辛くなってきたり、一つの変更が別の箇所に影響してしまい、意図しない動作を引き起こしてしまうなどということがあるのではないでしょうか。
自分もまだ ReSwift
や Redux
がどういうものがわかっていませんが、状態の遷移とビューへの反映の混在を分離するところから、まずはじめてみようと思っています。