はじめに
potatotips勉強会 #23 で @izumin5210 さんが"Predictable state container" and Data Bindingのタイトルで, Reduxにインスパイアされて開発したAndroid向けのRedux実装のDroiduxがとても興味深かったので、SwiftでもReduxにチャレンジしてみることにしました。
Reduxは複雑になりがちなアプリの状態やデータを然るべきルールの元で管理することで、アプリ開発の複雑性を軽減し品質とメンテナンス性のあるコードへと寄与してくれます。この考え方はJavaScriptのアプリ(SPA)に留まらずiOSアプリでも通ずるように感じました。
SwiftでのRedux実装を探したところ3つほど見つかりました。どれもGithubStarはまだ一桁台です。今回はAleksionさん作のReduxSwiftをさわってみます。ReduxSwiftはstateのsubscribeにRxSwiftを活用しています。
Reduxについて
Redux is a predictable state container for JavaScript apps.
ReduxはJavaScriptアプリケーションのための予測可能な状態コンテナです。
Reduxは以下の3原則に則って状態変化の流れを制限することで、状態を管理できるようにしています。
Three Principles (3原則)
Single source of truth
アプリケーション全体の状態(state)はツリーの形で1つのオブジェクトで作られ、1つのストアに保存される
State is read-only
状態を変更する手段は、変更内容をもったactionオブジェクトを発行して実行するだけ
Mutations are written as pure functions
アクションがどのように状態を変更するかを「Reducer」で行う
データフロー
Reduxのデータフローはシンプルな一方向のサイクルで表現されます。データ更新の手続きを「Reducer」という1つの窓口に集約することで、いつどのようにデータが更新されるのか把握しやすくなります。
図は Introduction to Redux // Speaker Deck からお借りしました。
ドキュメント
Reduxの詳細については本家のサイトやドキュメントを参照ください。和訳くださっている方がいるのでそちらもどうぞ。
本家
翻訳
- Redux入門 1日目 Reduxとは(公式ドキュメント和訳)
- Redux入門 2日目 Reduxの基本・Actions(公式ドキュメント和訳)
- Redux入門 3日目 Reduxの基本・Reducers(公式ドキュメント和訳)
- Redux入門 4日目 Reduxの基本・Stores(公式ドキュメント和訳)
- Redux入門 5日目 Reduxの基本・Data Flow(公式ドキュメント和訳)
- Redux入門 6日目 ReduxとReactの連携(公式ドキュメント和訳)
環境構築
以下の環境で行いました。
- Xcode7.1.1
- Swift2.1
新規プロジェクトを作成後、Carthageでライブラリをインストールします。
$ cat Cartfile
github "Aleksion/reduxSwift"
git "git@github.com:ReactiveX/RxSwift.git" "2.0.0-beta.2"
$ carthage update --platform ios
*** Cloning RxSwift
*** Cloning reduxSwift
*** Checking out RxSwift at "2.0.0-beta.2"
*** Checking out reduxSwift at "0.0.18"
*** xcodebuild output can be found in /var/folders/mg/ryg2v7mx28jbd1k_6xh0gmc00000gn/T/carthage-xcodebuild.yrb0dw.log
*** Building scheme "RxBlocking-iOS" in Rx.xcworkspace
*** Building scheme "RxCocoa-iOS" in Rx.xcworkspace
*** Building scheme "RxSwift-iOS" in Rx.xcworkspace
*** Building scheme "SwiftRedux" in SwiftRedux.xcodeproj
~/s/g/s/ReduxExample cat Cartfile
Buildしたライブラリをプロジェクトに追加します。
追加したライブラリをインストール時にコピーするようにします。
コーディング
SwiftReduxのUsageを参考にコーディングしました(ほとんどそのままです)。
Swiftで記述する場合JavaScript版に比べて以下の優位性があるかなと思いました。
- ActionとStateのデータを
let
で定義することでImmutableであることを明示できる - ActionのTypeに任意の一意な文字列をラベルを定義する必要がない
- Struct名が自動的にTypeとして設定される
- storeをsubscribeすることで得られるstateは型を有しているので、どのようなstateなのか判断できる
//
// ViewController.swift
// ReduxExample
//
// Created by susieyy on 11/29/15.
// Copyright © 2015 SUSIEYY. All rights reserved.
//
import UIKit
import SwiftRedux
import RxSwift
/**
* This is a simple standard action. The only requirement is that an action complies to
* the Action protocol. The SimpleStandardAction containts a strongly typed rawPayload
* property. The protocol automatically assigns the rawPayload to the Actions payload
* property. This removes the necessity of type casting whenever working with actions in * a reducer.
*
* There's also the StandardAction protocol, that requires the struc to have an
* initializer. This is required if the bindActionCreators helper is to be used.
*/
struct IncrementAction: SimpleStandardAction {
let meta: Any? = nil
let error: Bool = false
let rawPayload: Int = 1
}
/**
* This is a simple reducer. It is a pure function that follows the syntax
* (state, action) -> state.
* It describes how an action transforms the previous state into the next state.
*
* Instead of using the actions.type property - as is done in the regular Redux framework
* we use the power of Swifts static typing to deduce the action.
*/
func counterReducer(previousState: Int?, action: Action) -> Int {
// Declare the reducers default value
let defaultValue = 0
var state = previousState ?? defaultValue
switch action {
case let action as IncrementAction:
state = state + action.rawPayload
return state
default:
return state
}
}
/**
* The applications state. This should contain the state of the whole application.
* When building larger applications, you can optionally assign complex structs to
* properties on the AppState and handle them in the part of the application that
* uses them.
*/
struct AppState: State {
let count: Int!
}
/**
* Create the applications reducer. While we could create a combineReducer function
* we've currently chosen to allow reducers to be statically typed and accept
* static states - instead of Any - which currently forces us to define the
* application reducer as such. This could possibly be simplified with reflection.
*/
let applicationReducer = {(state: State? , action: Action) -> State in
// Optionally throw error if the given state isn't an AppState
let appState = state as! AppState?
return AppState(count: counterReducer(appState?.count, action: action))
}
// Create application store. The second parameter is an optional default state.
let store = createStore(applicationReducer, initialState: nil)
class ViewController: UIViewController {
@IBOutlet weak var countLabel: UILabel!
var disposable: Disposable? = nil
override func viewDidLoad() {
super.viewDidLoad()
disposable = store.subscribe{ (state: State) in
print(state)
if let state = state as? AppState {
self.countLabel.text = "\(state.count)"
}
}
}
deinit {
// Dispose of the subscriber after use.
disposable?.dispose()
}
@IBAction func incremntAction(sender: AnyObject) {
store.dispatch(IncrementAction())
}
}