ReSwiftを使うときに知っておくといいTips3選


前書き

最近AndroidでもiOSでもReduxアーキテクチャが気に入っており、アプリ開発時に(ごり押して)採用しているUroteaでございます。
Android向けReduxの記事も書いているので、Androidも書く人は読んでみてください。

ReSwiftを使ってアプリを書いていて気が付いたTips集

機会があり、iOSアプリを書くことになりました。

ログインしたユーザの権限によってボタンを非表示にしたり、レイアウトを変えたりする必要があるため、状態管理に強いReSwiftを選択しました。
ある程度作り終わった今、この選択は正解だったと思っています。
また、ViewとのbindingのためにRxSwiftも併用しましたが、こちらも採用してよかったと思っています。
そんな中で気が付いたTipsを共有したいと思います。

この記事で書くこと

  • ReSwiftを使うときに、知っておくと便利なこと
  • RxSwiftとどう連携したか

この記事で書かないこと

  • ReSwiftの概念、使い方
  • RxSwiftの概念、使い方

対象読者

  • iOS開発の基本を知っている
  • Swiftがある程度読める
  • Reduxの基本を知っている
  • ReSwiftの基本は知っている
  • RxSwiftについてなんとなく知っている
  • MVVMについてなんとなく知っている

Tips1: Actionはenumで定義する

型で区切ることで管理を容易にする

Reduxの概念の一つActionですが、ReSwiftのreadmeには以下のように書かれています。

struct CounterActionIncrease: Action {}
struct CounterActionDecrease: Action {}

もちろんこれでもいいのですが、Actionが100個くらいになると、どのActionがどのUIのイベントに対応しているのかの管理が困難になります。と言うか無理です。
ある程度なんらかの単位に切り分けて管理しなければ私の脳では処理ができませんでした。
私は画面ごとに分割することにしました(分割単位や粒度は場合によると思います)。


enum Actions: Action {
    case loginViewActions(LoginViewActions)
    case mainViewActions(MainViewActions)
}

enum LoginViewActions {
    case loginButtonTapped
    case userNameTextChanged(userName: String)
}

enum mainViewActions {
    // 省略
}

このように分割すると
- それぞれのActionがどの画面に対応しているのか型で判別可能
- Actionの名前衝突問題を気にしなくて良い

Reducerでパターンマッチングが効く

さらに、Reducerでも力を発揮します。公式のreadmeでは以下のように書かれています

func counterReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()

    switch action {
    case _ as CounterActionIncrease:
        state.counter += 1
    case _ as CounterActionDecrease:
        state.counter -= 1
    default:
        break
    }

    return state
}

こちらの欠点としては、新たなActionを追加したときに、Reducerに変更を加えるのを忘れてもコンパイルエラーにならないことです。
Reducerのテストは書くと思うので、大丈夫だと思いますが気が付けるに越したことはありません。
enumを採用すると以下のようになります。

func counterReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()

    switch action {
    case loginViewActions(let action):
        switch action {
            case loginButtonTapped:
                // なんらかの処理
            case userNameTextChanged(let userName):
                // なんらかの処理
        }
    case mainViewActions(let action):
        switch action {
            // 省略
        }
    }
}

enumにすることでdefault句を消すことができ、新しいActionを増やすとコンパイルで弾かれるようになります。
また、どの部分でどのActionの処理をしているのかを見やすくなるので、可読性が向上します。

Tips2: RxSwiftのdistinctUntilChangedを使用してViewに状態の変更を伝える

RxSwiftにはRxCocoaというViewと連携するときに便利なライブラリがついており、これを活用します。
今回はMVVMアーキテクチャを採用した場合のサンプルです。


class LoginViewModel {
    private let loginButtonEnabledOutputStream = PublishRelay<Bool>
    let loginButtonEnabled: Signal<Bool> {
        self.loginButtonEnabledOutputStream.asSignal().distinctUntilChanged()
    }
}

extension LoginViewModel: StoreSubscriber {
    typealias StoreSubscriberStateType = AppState

    func newState(state: AppState) {
        self.loginButtonEnabledOutputStream(state.login.buttonEnabled)
    }
}

ポイントはdistinctUntilChanged()です。ReSwiftのstateはstateが更新されるたびに呼び出されます。
上記の例ですと、loginButtonの状態が更新されていなくても、state全体としては更新されているとnewState(state: AppState)が呼び出されます。
ボタンをenableからenableにするなら問題はありませんが、dialogを表示する処理などをReSwiftに乗っけると、何かあるたびにdialogが表示されてしまいます。
distinctUntilChanged()を挟んでおくことで、false -> trueなど、状態が変化した時のみViewに通知されるようになります。

Tips3: stateのstructにcopy()を実装しておく

Kotlinを触ったことがある人向けに説明すると、data classのcopyメソッドです。
触ったことがない人向けに説明すると、以下のようなメソッドです。

let loginState = LoginState(/*省略*/)

let newLoginState = loginState.copy(loginButton: true)
// newLoginStateはloginStateのloginButtonだけtrueにし、他のフィールドは全て同じ値のstruct

ReduxのReducerでは前のstateから新しいstateを生み出すコードを書きます。
しかし、状態が複雑な場合、前の状態から新しい状態を全てについて考えていると脳がパンクします。
copyメソッドを使用することで、前の状態との差分のみ更新することができるようになり、脳のメモリが節約できます。

func counterReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()

    switch action {
    case loginViewActions(let action):
        switch action {
            case loginButtonTapped:
                // ログインボタンがタップされたので、押せなくする
                return state.copy(loginButton: false)
        }
    }
}

Swiftにはcopy()を自動的に作ってくれる機構はないので、頑張って書くしかありません。
私は以下のように書きました。


struct AppState {
    let loginButton: Bool
    let loginUserName: String?

    func copy(
        loginButton: Bool? = nil,
        loginUserName: String?? = nil
    ) {
        return AppState(
                    loginButton = loginButton ?? self.loginButton,
                    loginUserName = loginUserName ?? self.loginUserName
               )
}

こうすることで、copyメソッドで指定しなかった値はcopy元の値が使用されるようになります。
唯一の弊害として、明示的にnilを入れたい場合は以下のようにしなければなりません。
普通にnilを代入するとcopy元の値が使用されてしまう!!

let state = AppState(/*省略*/)
state.copy(loginUserName: .some(nil))  // 明示的にloginUserNameにnilを入れてcopyする方法

ちょっと気をつけて使う必要がありますが、とても便利でした。ただ、copyメソッドを書くのが面倒です...

まとめ

iOSアプリをReSwiftを活用してがっつり書いてみて学んだTips3選でした。
- Actionはenumで定義する
- RxSwiftと連携する場合はdistinctUntilChanged()を活用する
- stateにcopyメソッドを実装する

書き始める前に知っているだけで簡単に導入できると思うので、ぜひご活用ください。