はじめに
業務で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 は関数を所有しないデータのみで表現されるシンプルなオブジェクトで構成されます。
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 の変更を実施しています。
2
Changes are made with pure functions / 純粋関数
新たな State の作成を担う Reducer は 関数として表現します。
ここでいう関数とは、オブジェクト指向なクラスやメソッドによる記述ではなく、Reducer 自身が関数(Reducer 関数) として記述されます。
さらに、その関数は 純粋関数であることが求められます。
つまり、Reducer関数の中で新たなStateを作って返すという処理になります。
3
純粋関数とは
純粋関数は以下の特徴を持った関数です。
- 与えられた要素や関数外の要素を変化させない
- 戻り値以外の出力を行わない
- 取り扱うすべての要素が引数として宣言されている
- 入力に対して出力が常に一意である(同じ入力には常に同じ出力を返す)
以下のコードが簡単な純粋関数の例です。
// 純粋関数の例:引数として与えられた整数を2倍にして返す関数
func doubleValue(_ value: Int) -> Int {
return value * 2
}
// 使用例
let number = 5
let doubledNumber = doubleValue(number)
print(doubledNumber) // 出力: 10
Reduxの概念図
ここまでの話をまとめると以下の図のようになります。
- ユーザーの入力をもとにActionを作成
- ActionをStoreへDispatch(送信)する
- ActionをもとにReducerがStateを更新
- StateをもとにUIを更新
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の導入を考えている方に参考になれば幸いです。
-
iOS設計パターン入門 p168 ↩
-
iOS設計パターン入門 p169 ↩
-
iOS設計パターン入門 p170 ↩
-
https://redux.js.org/tutorials/fundamentals/part-1-overview ↩