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

SwiftでReduxをさわってみる

More than 1 year has passed since last update.

はじめに

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つの窓口に集約することで、いつどのようにデータが更新されるのか把握しやすくなります。

名称未設定.jpg

図は Introduction to Redux // Speaker Deck からお借りしました。

ドキュメント

Reduxの詳細については本家のサイトやドキュメントを参照ください。和訳くださっている方がいるのでそちらもどうぞ。

本家

翻訳

環境構築

以下の環境で行いました。

  • 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したライブラリをプロジェクトに追加します。

ReduxExample_xcodeproj.jpg

追加したライブラリをインストール時にコピーするようにします。

ReduxExample_xcodeproj.jpg

コーディング

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

動作

11月 29, 2015 23:33.gif

Refs

Redux

Swift Redux

翻訳

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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