LoginSignup
7
10

More than 5 years have passed since last update.

RxSwiftのsamplesを見ていたらRedux的なものを見つけた

Last updated at Posted at 2017-03-14

背景

最近個人的な興味本位で、React+Reduxの学習をはじめていました。

SwiftもRx化の波が本格襲来してそうな(した?)感覚を肌で感じたのでお勉強を開始しました。
その中で、Rxは難しいのだけどもRedux経験者にわかりやすいsampleソースを見つけたというお話です。

メモ書きとして残しておきます。

モチベーション

rxの勉強のため、自分の理解を高める為に記事にまとめる。
やったことを忘れないようにするため。

Reduxの設計をざっくり整理してみる

  • stateという状態を中心にした設計である
  • stateはActionと前state(preStateと呼ぶ)によりstateを適切なものに変更する←これがReducerの役割
  • component(画面のパーツ)がstateの中の要素を参照していて、その値が書き換わったら勝手に(?)Virtual Domの内容を書き換える

個人的にはこのstateとactionの関係&stateを参照するcomponentのつながりが、すごくハマりました。

また、stateを変更するには必ずactionを介さないといけないので、それもシンプルですごくクリアでした。

グローバル変数でいたるところで書き換えられる可能性があるとカオスりそうで、
Reducerを見れば何が原因でStateが変わったかがはっきりするので追いやすいと思います。

Rxのサンプルを読み解く

ざっくり構成

3/14現在、Calculatorというサンプルがあります。

Calculator.png

電卓アプリを題材にしたサンプルです。

  • CalculatorViewController :メインの処理部分です。
  • CalculatorState :Reduxのstateに当たる部分です。
  • CalculatorAction :ReduxのActionに当たる部分です。
  • Operator : Actionで利用されるもの(Actionの処理を一部共通化したいために用意したもの)

CalculatorState

Reduxで言うところのReducerの部分

// reduxのstateのデータ宣言部分ですね。
// stateはこの構造体として保持します。
struct CalculatorState {
    // 初期時、電卓リセット時に利用。
    static let CLEAR_STATE = CalculatorState(previousNumber: nil, action: .clear, currentNumber: "0", inScreen: "0", replace: true)

    let previousNumber: String!
    let action: Action
    let currentNumber: String!
    let inScreen: String
    let replace: Bool
}

// ReduxのReducer部分ですね。
// reduxのreducerはstateとactionを引数に処理しますが
// こちらはstructのfunctionなのでstateは保持されているので引数渡ししておりません。
extension CalculatorState {
    // actionを引数に、pre stateとactionでstate(CalculatorState)を返却
    func tranformState(_ x: Action) -> CalculatorState {
        switch x {
        case .clear:
            return CalculatorState.CLEAR_STATE
        case .addNumber(let c):
            return addNumber(c)
        case .addDot:
            return self.addDot()
        case .changeSign:
            let d = "\(-Double(self.inScreen)!)"
            return CalculatorState(previousNumber: previousNumber, action: action, currentNumber: d, inScreen: d, replace: true)
        case .percent:
            let d = "\(Double(self.inScreen)!/100)"
            return CalculatorState(previousNumber: previousNumber, action: action, currentNumber: d, inScreen: d, replace: true)
        case .operation(let o):
            return performOperation(o)
        case .equal:
            return performEqual()
        }
    }

    // 数字を入力したときに呼ばれる処理
    func addNumber(_ char: Character) -> CalculatorState {
        let cn = currentNumber == nil || replace ? String(char) : inScreen + String(char)
        // 構造体の再構築してますね。構造体の中にあるfunctionだから、
        // 変更しないstateは、previousNumberや、actionはそのまま設定して、
        // cnなどは、上で処理したものを設定
        // データが構造体であって作り直されていることで、
        // preStateとcurrentStateが別物なのもreduxぽいですね
        return CalculatorState(previousNumber: previousNumber, action: action, currentNumber: cn, inScreen: cn, replace: false)
    }

    (省略

Action

reduxのActionもすごくシンプルでしたが、それに輪をかけてこちらは簡素ですね。
どんなAction(実体はない)をやるかだけ書いているのはすごくわかりやすい。
電卓はもっとボタンがあるのですが、
case operation(Operator)
で(+, -, ×, / )のオペレーションを共通化しているので、Actionとしてはこれだけです。

enum Action {
    case clear
    case changeSign
    case percent
    case operation(Operator)
    case equal
    case addNumber(Character)
    case addDot
}
enum Operator {
    case addition
    case subtraction
    case multiplication
    case division
}

extension Operator {
    var sign: String {
        switch self {
        case .addition:         return "+"
        case .subtraction:      return "-"
        case .multiplication:   return "×"
        case .division:         return "/"
        }
    }

    var perform: (Double, Double) -> Double {
        switch self {
        case .addition:         return (+)
        case .subtraction:      return (-)
        case .multiplication:   return (*)
        case .division:         return (/)
        }
    }
}

CalculatorViewController

Reduxの機構を構築しているメイン部分です。

    override func viewDidLoad() {
        let commands:[Observable<Action>] = [
            allClearButton.rx.tap.map { _ in .clear },

            changeSignButton.rx.tap.map { _ in .changeSign },
            percentButton.rx.tap.map { _ in .percent },

            divideButton.rx.tap.map { _ in .operation(.division) },
            multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
            minusButton.rx.tap.map { _ in .operation(.subtraction) },
            plusButton.rx.tap.map { _ in .operation(.addition) },

            equalButton.rx.tap.map { _ in .equal },

            dotButton.rx.tap.map { _ in  .addDot },

            zeroButton.rx.tap.map { _ in .addNumber("0") },
            oneButton.rx.tap.map { _ in .addNumber("1") },
            twoButton.rx.tap.map { _ in .addNumber("2") },
            threeButton.rx.tap.map { _ in .addNumber("3") },
            fourButton.rx.tap.map { _ in .addNumber("4") },
            fiveButton.rx.tap.map { _ in .addNumber("5") },
            sixButton.rx.tap.map { _ in .addNumber("6") },
            sevenButton.rx.tap.map { _ in .addNumber("7") },
            eightButton.rx.tap.map { _ in .addNumber("8") },
            nineButton.rx.tap.map { _ in .addNumber("9") }
        ]

        Observable.from(commands)
            .merge()
            .scan(CalculatorState.CLEAR_STATE) { previous, action in
                previous.tranformState(action)
            }
            .debug("calculator state")
            .subscribe(onNext: { [weak self] calState in
                self?.resultLabel.text = self?.formatResult(calState.inScreen)

                if case let .operation(operation) = calState.action {
                    self?.lastSignLabel.text = operation.sign
                } else {
                    self?.lastSignLabel.text = ""
                }
            })
            .disposed(by: disposeBag)
    }

    func formatResult(_ result: String) -> String {
        if result.hasSuffix(".0") {
            return result.substring(to: result.index(result.endIndex, offsetBy: -2))
        } else {
            return result
        }
    }

長いので分割しながらまとめてみます

Dispatch(Action)部分

reduxで言うところの、onClickからdispatch(Action)を呼び出す部分
をまとめて宣言しているところですかね。
swiftでは、@IBOutletのActionをまとめて一箇所で書けるのはすごくすっきりでわかりやすいです。
reduxもこのように書けると素敵だな。(余談)

電卓の各ボタンを押下したときのイベントをActionに置き換えている部分ですね。
map関数がそれぞれのボタン(rx.tap)をActionに変換してます。

   let commands:[Observable<Action>] = [
            allClearButton.rx.tap.map { _ in .clear },

            changeSignButton.rx.tap.map { _ in .changeSign },
            percentButton.rx.tap.map { _ in .percent },

            divideButton.rx.tap.map { _ in .operation(.division) },
            multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
            minusButton.rx.tap.map { _ in .operation(.subtraction) },
            plusButton.rx.tap.map { _ in .operation(.addition) },

            equalButton.rx.tap.map { _ in .equal },

            dotButton.rx.tap.map { _ in  .addDot },

            zeroButton.rx.tap.map { _ in .addNumber("0") },
            oneButton.rx.tap.map { _ in .addNumber("1") },
            twoButton.rx.tap.map { _ in .addNumber("2") },
            threeButton.rx.tap.map { _ in .addNumber("3") },
            fourButton.rx.tap.map { _ in .addNumber("4") },
            fiveButton.rx.tap.map { _ in .addNumber("5") },
            sixButton.rx.tap.map { _ in .addNumber("6") },
            sevenButton.rx.tap.map { _ in .addNumber("7") },
            eightButton.rx.tap.map { _ in .addNumber("8") },
            nineButton.rx.tap.map { _ in .addNumber("9") }
        ]

単一のObservableにしている


        // 上は配列なので、配列をObservableとして受け取るのに.from(commands)
        // 単一項目ならjust、複数要素ならofですね。
        Observable.from(commands)
            // mergeを使ってますね。これは、オフィシャルをみると複数のObserbale sequencesここでは配列の要素をSingle Obaservableに変換しています。
            .merge()

ちなみにmergeで単一のObservableにしているのは、何故だろうと考えましたが、個人的には以下のように理解しました。

  • 電卓は同時押しがない。(そのように扱いたいため?)
  • stateの状態を一元化する為
  • 一つのstreamで記述したかった為

Reduxの機構を作り出している部分(Scan)

今回記事としてまとめるきっかけになった処理部分。
scanてなんだろうと調べていたら、playgroundにわかりやすいサンプルがありましたので
以下に記載します。

           .scan(CalculatorState.CLEAR_STATE) { previous, action in
                previous.tranformState(action)
            }

Scan (Playgroundより転機)

オフィシャルを読むと、初期値を伴って、
各Sequence(10,100,1000)を実行していく

ポイントは、前の結果を引き継いで、次のSequeceを放出(emit)する。

だから、
10 + 1 = 11 をemit
100 + 11(前回値) = 111 をemit
1000 + 111 = 1111 をemit

Observable.of(10, 100, 1000)
        // 前回emitした値ここでは、aggregateValueが次のsequenceをemitするときに渡される
        .scan(1) { aggregateValue, newValue in
            aggregateValue + newValue
        }
        .subscribe(onNext: { print($0) })
        .disposed(by: disposeBag)

// output
11
111
1111

元のサンプルに戻ると、なるほどscanはemitするだけではなく
前の状態を引き継ぎ次回のemitする性質があるから、
reduxのpreStateを保持する仕組みを作ることが出来るわけなんですね。

playgroundのscanを見ただけでは、そこまでの活用方法が見いだせませんでしたが、
calculatorのサンプルはすごく活用方法をイメージさせるものでした。

reduxで言うところの、componentとstateを関連付けしている部分

scanからemitされた、calState(state)が渡されて(購読され)て、
電卓の表示に反映されている。

reduxではmapStateToPropsでstateからデータを抜き出して、パラメータ渡しして、
render()なりで結びつけているのをswiftだと一箇所に書けるのは楽だなと実感しました。

            .subscribe(onNext: { [weak self] calState in
                self?.resultLabel.text = self?.formatResult(calState.inScreen)

                if case let .operation(operation) = calState.action {
                    self?.lastSignLabel.text = operation.sign
                } else {
                    self?.lastSignLabel.text = ""
                }
            })
            .disposed(by: disposeBag)

最後に

reduxとか全然関係ないプログラム言語の思想がRxSwiftのサンプルでも見つけられたので、
改めてつながってるなと実感しました。
勉強になりました。

7
10
2

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
7
10