7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iOSAdvent Calendar 2023

Day 16

Redux for iOS App

Last updated at Posted at 2023-12-15

はじめに

業務でReduxを採用した案件に携わってだいぶ時間も経ったため、アウトプットとして書いていきます。
本記事では、iOSアプリ開発におけるReduxアーキテクチャの開発の基本について解説します。

Reduxとは

Reduxとは、そもそもReact.jsでstate(状態)を管理をするためのフレームワークです。
iOSではReduxを実現するために使用されるライブラリとして、ReSwiftがあります。今回はReSwiftを題材として説明を進めます。

Reduxの利点

個人的に上げる利点は以下の2つです。

  • 一貫性のある状態管理
  • 単一方向のデータフロー

アプリケーションの状態を一つのStoreで管理するため、一貫性が保たれます。
また、アプリケーション内でのデータの流れが単一方向であるため、データフローがシンプルで理解しやすくなります。
これにより、デバッグやテストが容易になります。

Reduxの基本概念

Reduxには以下の3つの原則が存在します。

  • Single source of truth / 信頼できる唯一の状態
  • State is read-only / 状態はイミュータブル
  • Changes are made with pure functions / 純粋関数

Single source of truth / 信頼できる唯一の状態

Redux では各状態のインスタンスがあちこちに分散することなく、アプリケーション全体の状態を単一のオブジェクトツリーで管理します。
この状態のオブジェクトツリー(図 9.2)を State と呼びます。State は関数を所有しないデータのみで表現されるシンプルなオブジェクトで構成されます。

image.png
1

State is read-only / 状態はイミュータブル

ReduxではStateを直接変更することはできません。
では、どのようにして State の変更を行えばよいのでしょうか。
State の変更 は Action がディスパッチを介して Reducer のみ実行できるように制約されています。
図 9.3 では Reducer による State の変更手続きを示しています。
Reducer は現在の State とディスパッチされた Action の2つを入力に受け、新しい State を 出力する関数です。
現在の State はイミュータブルであるため値の変更を行わず、現在の State のコピーを作ります。
Reducer に記述されたビジネスロジックの実行結果をコピーした State に適用し、新たな State として出力します。このように Reducer では State の変更を実施しています。
image.png
2

Changes are made with pure functions / 純粋関数

新たな State の作成を担う Reducer は 関数として表現します。
ここでいう関数とは、オブジェクト指向なクラスやメソッドによる記述ではなく、Reducer 自身が関数(Reducer 関数) として記述されます。
さらに、その関数は 純粋関数であることが求められます。
つまり、Reducer関数の中で新たなStateを作って返すという処理になります。
image.png
3

純粋関数とは

純粋関数は以下の特徴を持った関数です。

  • 与えられた要素や関数外の要素を変化させない
  • 戻り値以外の出力を行わない
  • 取り扱うすべての要素が引数として宣言されている
  • 入力に対して出力が常に一意である(同じ入力には常に同じ出力を返す)

以下のコードが簡単な純粋関数の例です。

// 純粋関数の例:引数として与えられた整数を2倍にして返す関数
func doubleValue(_ value: Int) -> Int {
    return value * 2
}

// 使用例
let number = 5
let doubledNumber = doubleValue(number)
print(doubledNumber) // 出力: 10

Reduxの概念図

ここまでの話をまとめると以下の図のようになります。

  1. ユーザーの入力をもとにActionを作成
  2. ActionをStoreへDispatch(送信)する
  3. ActionをもとにReducerがStateを更新
  4. StateをもとにUIを更新
    redux_date_flow.gif
    4

Stateの定義

アプリケーションの状態を表すStateを定義します。Stateはイミュータブルなデータ構造として表現され、アプリケーション全体の状態を保持します。

この例では、AppStateという構造体を定義しています。
AppStateはStateTypeプロトコルに準拠しており、counterというプロパティを持っています。
ReSwiftの機能であるStateTypeを使うと、アプリ側でReduxのStateとして扱うことができます。

struct AppState {
    var count: Int = 0
}

Actionの定義

この例では、IncrementActionとDecrementActionという2つのActionが定義されています。これらのアクションは、カウンターの値を増やすか減らすために使用されます。

extension AppState {
    struct IncrementAction: Action {}
    struct DecrementAction: Action {}
}

Reducerの定義

この例では、appReducerというレデューサー関数が定義されています。
関数は2つのパラメーターを受け取ります:ActionとStateです。
Stateは現在のアプリケーションのステートを表し、Actionは発行されたアクションです。

Reducerの定義内では、switch文を使用してアクションの種類を判別し、それに応じたステートの変更を行います。
この例では、IncrementActionが発行された場合はcounterを1増やし、DecrementActionが発行された場合はcounterを1減らします。

最後に、変更されたステートを返しています。

このようにReducerを作成することで、Actionに応じてStateが変化し、アプリケーションの状態が管理されます。

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

    switch action {
    case is AppState.IncrementAction:
        state.count += 1
    case is AppState.DecrementAction:
        state.count -= 1
    default: break
    }

    return state
}

Storeの定義

Storeはシングルトンパターンで実装します。
シングルトンで実装することで、アプリ内部で唯一のStoreであることが保証されます。
このシングルトンで実装されたStoreを使ってStateをViewで参照したり、UIイベントが発生した時にActionをdispatchします。

final class AppStore {
    static let shared = AppStore()

    let store: Store<AppState>

    private init() {
        store = Store<AppState>(reducer: appReducer, state: nil)
    }
}

Viewでの使い方

こちらがViewへの適応例です。
incrementButtonとdecrementButtonの2つを画面に配置します。
これらのボタンを押すと、Stateで保持しているcountプロパティを増減させることができます。

incrementButton.rx.tap
    .subscribe(onNext: { [weak self] _ in
            guard let self = self else { return }
            store.dispatch(AppState.IncrementAction())
            self.counterLabel.text = "\(store.state.count)"
        })
        .disposed(by: disposeBag)

decrementButton.rx.tap
    .subscribe(onNext: { [weak self] _ in
            guard let self = self else { return }
            store.dispatch(AppState.DecrementAction())
            self.counterLabel.text = "\(store.state.count)"
        })
        .disposed(by: disposeBag)

例えば、incrementButtonが押されたときは以下のような挙動をします。

  • incrementButtonが押されると、IncrementActionを発行
  • IncrementActionが発行されると、Reducerが呼ばれてReducerが新たなStateを生成
  • Reducerが呼ばれることで、Stateのcountプロパティの値が1増える

まとめ

本記事では、iOSアプリ開発におけるReSwiftを利用したReduxアーキテクチャの開発の基本について解説しました。

GitHubにもサンプルコードを置いているので、興味のある方はぜひ覗いてみてください!

他のアーキテクチャと比較しても学習コストが大きいReduxですが、一貫性のある状態管理と単一方向のデータフローを実現できるかなり強力なアーキテクチャなので、Reduxの導入を考えている方に参考になれば幸いです。

  1. iOS設計パターン入門 p168

  2. iOS設計パターン入門 p169

  3. iOS設計パターン入門 p170

  4. https://redux.js.org/tutorials/fundamentals/part-1-overview

7
1
0

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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?