ReSwiftでアプリの状態管理のスライドを見てReSwiftなるものがあると知り面白そうだったので使ってみました
当方、JSあまり触ったことがなく、FluxもReduxもよくわからん状態でReSwiftの実装を見て説明しているだけなので間違いなどあればご指摘ください
Reduxとは
ReduxはJavaScripアプリケーションのための予測可能な状態コンテナです
Reduxは3原則に則って状態変化の流れを制限することで、複雑状態の管理を可能にします
Reduxの3原則
- Single source of truth(ソースは1つだけ)
- アプリケーション全体の状態(State)はツリーの形で1つのオブジェクトで作られ、1つのストアに保存される
- State is read-only(状態は読み取り専用)
- 状態を変更する手段は、変更内容をもったactionオブジェクトを発行して実行するだけ
- Mutations are written as pure functions(変更はすべてpureな関数で書かれる)
- アクションがどのように状態を変更するかを「Reducer」で行う
Reduxの登場人物
Store
- Storeはアプリケーション内で1つだけ存在し、1つのアプリケーションの状態(State)を保持する
- StateへアクセスするためのgetState()を提供する
- Stateを更新するためのdispatch(action)を提供する
- リスナーを登録するためのsubscribe(listener)を提供する
State
- アプリケーションの状態を表す
Action
- Storeが保持しているStateの変更内容が記載されているオブジェクト
- Actionは
store.dispatch()
でStoreへ送られる
ActionCreator
- Actionを作成する
Reducer
- ActionとStateから、新しいStateを作成して返す
- Stateを更新することはせず、新しいStateのオブジェクトを作成して返す
Stateを変更する処理の流れ
Stateの変更内容が記載されているActionを発行し、Storeが提供しているdispatch(Action)
メソッドを通してStoreにActionを送る
Storeは送られてきたActionと保持しているStateをReducerに渡し、Reducerが新たなStateを生成しStoreのがStateを保持する
ほとんどこちらの記事を参考、引用させていただきました
ReSwiftとは
上記で説明したReduxをSwiftで実現することのできるライブラリです
https://github.com/ReSwift/ReSwift
作ったサンプルアプリについて
ということでReSwiftを使って簡単なサンプルアプリを作ってみました
ReduxSwiftQiitaClient
このアプリは以前投稿したMVVMっぽい構成のデモアプリを公開してみると概ね同じものをReSwiftを使って実装したものです
画面
画面数は4つで下記の画面が存在します
ホーム(投稿一覧) | 投稿詳細 | ユーザー投稿一覧 | ユーザー投稿詳細 |
---|---|---|---|
画面ごとの要件や構成
- ホーム画面(投稿一覧)
- Qiitaの全投稿一覧が表示される
- 一度に読み込まれる投稿は20件
- 最下部までスクロールすると次の20件を読み込む
- セルをタップすると、タップした投稿の詳細画面へ遷移する
- 投稿詳細画面
- 投稿の詳細が表示される
- ユーザー名のリンクをタップすると、そのユーザーの投稿一覧画面に遷移する
- ストックボタンをタップすると投稿をストックor解除できる
- 本文はWebViewで表示されている
- ユーザー投稿一覧画面
- 選択したユーザーの投稿一覧が表示される
- あとはホーム画面(投稿一覧)と同じ
- ユーザー投稿詳細
- 投稿詳細画面と同じだが、ユーザー名のリンクは存在しない
今回作成したサンプルアプリの動かし方
とりあえずサンプルコード見て理解したいという方はここに今回作成したサンプルコードがあります
ビルドが通るようになる一連の作業としては
git clone [email protected]:hachinobu/ReduxSwiftQiitaClient.git
でcloneして、cd ReduxSwiftQiitaClient
でプロジェクト内に移動してください
その後にpod install
を叩いてライブラリを入れてください
また、サンプルアプリを動かすにはQiitaのアクセストークンが必要です
アクセストークンは、この取得画面から発行できます
スコープは下記スクリーンショットの通り、read_qiita
とwrite_qiita
にチェックをつけて「発行する」ボタンから発行してください
アクセストークンが発行されたら、それをコピーしてReduxSwiftQiitaClientプロジェクト内にSecrets.plistという名前のplistファイルを作成して
Dictionary TypeでAccessToken: 'Qiitaアクセストークン' となるようにしてください
(下記スクリーンショットのhogehoge部分を先ほど取得したアクセストークンに差し替えてください)
これでビルドして成功すればアプリを使うことができます
ReSwiftを使った実装(サンプルアプリ)の説明
ReSwiftは当然Reduxの概念に基づいているのでStore,State,Reducer,Actionが存在します
アプリケーションの状態(State)を保持するStoreを生成する必要がありますが、StoreはStateとReducerを先に作っておかないと生成できないので、まずはホーム画面を例にしてStateを設計してみます
ホーム画面のState
ホーム画面の表示項目に必要なものやAPI通信などを考慮して設計してみました
struct HomeState: ArticleListScreenStateProtocol {
var pageNumber: Int = 1
var articleList: [ArticleModel]?
var errorMessage: String?
var isRefresh: Bool = false
var showMoreLoading: Bool = false
}
HomeStateの各プロパティの意味についてです
-
pageNumber
- 現在何ページ目のデータを読み込んだかの情報です。最下部までスクロールすると次のページの情報を読み込む必要があるためです
-
articleList
- APIの返り値のオブジェクトです。ホーム画面の表示項目に必要なタイトルやタグ、ユーザー情報などが格納されているオブジェクトです
-
errorMessage
- 通信エラー時のメッセージを格納します。
-
isRefresh
- データをクリアしてデータ取得中かの判定フラグです
-
showMoreLoading
- ページング処理(次の◯件読み込み)中かの判定フラグです
また、HomeStateが準拠しているArticleListScreenStateProtocol
ですが、これは私が作成した独自のプロトコルですので、Stateを生成するうえで必要なものではありません
Stateを生成するだけならstruct HomeState {
で問題ありません
ArticleListScreenStateProtocol
については、後ほど説明しますので、一旦忘れてください
ホーム画面のAction
Stateに変更を加えるのに必要なActionです
ActionはReSwiftで定義されているActionプロトコルに準拠する必要があります
ReSwiftのActionプロトコルはActionとして判定するために定義されているだけで中身はありません
public protocol Action { }
下記がHomeStateのAction群です
extension HomeState {
struct HomeRefreshAction: Action {
let isRefresh: Bool
let pageNumber: Int
}
struct HomeArticleResultAction: Action {
let result: Result<GetAllArticleEndpoint.Response, SessionTaskError>
}
struct HomeShowMoreLoadingAction: Action {
let showMoreLoading: Bool
}
struct HomeMoreArticleResultAction: Action {
let result: Result<GetAllArticleEndpoint.Response, SessionTaskError>
}
}
ActionはStateの存在ありきのオブジェクトなのでHomeStateのextensionとして記載しています
-
HomeRefreshAction
- HomeStateの
isRefresh
,pageNumber
をまとめて変更する情報を持つAction - データ取得(PullToRefresh含む)開始/終了時に発行
- HomeStateの
-
HomeShowMoreLoadingAction
- HomeStateの
articleList
もしくはerrorMessage
を変更する情報を持つAction - Result型の投稿一覧取得APIの結果(成功or失敗)情報が格納される
- 投稿一覧APIのレスポンスを受け取った際に発行
- HomeStateの
-
HomeShowMoreLoadingAction
- HomeStateの
showMoreLoading
を変更する情報を持つAction - ページング処理(次の◯件読み込み)の開始/終了時に発行
- HomeStateの
-
HomeMoreArticleResultAction
- HomeStateの
articleList
に追加するデータを持つAction - ページング処理(次の◯件読み込み)の◯ページ目の投稿一覧APIのレスポンスを受け取った際に発行
- HomeStateの
ホーム画面のReducer
次に現在のStateとActionをもとに新たなStateを生成するReducerです
ReducerはReSwiftのReducerプロトコルに準拠する必要あります
import Foundation
public protocol AnyReducer {
func _handleAction(action: Action, state: StateType?) -> StateType
}
public protocol Reducer: AnyReducer {
typealias ReducerStateType
func handleAction(action: Action, state: ReducerStateType?) -> ReducerStateType
}
extension Reducer {
public func _handleAction(action: Action, state: StateType?) -> StateType {
return withSpecificTypes(action, state: state, function: handleAction)
}
}
上記のようになっているので、各Reducerは、Reducerプロトコルのfunc handleAction(action: Action, state: ReducerStateType?) -> ReducerStateType
を実装すれば良いです
それではHomeStateを変更するActionをもとに新たなStateを生成するHomeReducerです
import Foundation
import ReSwift
struct HomeReducer {
}
extension HomeReducer: Reducer {
func handleAction(action: Action, state: AppState?) -> AppState {
let state = state ?? AppState()
var newState = state
var homeState = newState.home
switch action {
case let action as HomeState.HomeRefreshAction:
homeState.updateIsRefresh(action.isRefresh)
homeState.updatePageNumber(action.pageNumber)
case let action as HomeState.HomeArticleResultAction:
switch action.result {
case .Success(let articleList):
homeState.updateArticleList(articleList.articleModels)
homeState.updateErrorMessage(nil)
case .Failure(let error):
switch error {
case .ResponseError(let qiitaError as QiitaError):
homeState.updateErrorMessage(qiitaError.message)
default:
homeState.updateErrorMessage("通信処理でエラーが発生しました")
}
}
case let action as HomeState.HomeMoreArticleResultAction:
if let moreArticleList = action.result.value {
homeState.appendArticleList(moreArticleList.articleModels)
}
case let action as HomeState.HomeShowMoreLoadingAction:
homeState.updateShowMoreLoading(action.showMoreLoading)
default:
break
}
newState.home = homeState
return newState
}
}
ReducerのhandleAction
メソッドにActionと現在のStateが引数で渡されます
引数、state
の型や返り値であるAppState
はアプリケーション全体のStateです
Reduxの原則である
- Single source of truth(ソースは1つだけ)
- アプリケーション全体の状態(State)はツリーの形で1つのオブジェクトで作られ、1つのストアに保存される
の部分に当てはまるもので、後述しますが、AppStateがHomeStateなど本アプリを構成する上で必要なState群を持ちます
HomeStateだけ作った現在だと下記です
import Foundation
import ReSwift
struct AppState: StateType {
var home = HomeState()
}
HomeReducerのhandleAction
メソッド内の説明を続けます
let state = state ?? AppState()
var newState = state
var homeState = newState.home
Reducerはあくまでも現在のStateを更新するのではなく、新たにStateを作るので上記コードになっています
そして発行されたActionを判別して、新たに生成したStateに変更を当てていきます
switch action {
case let action as HomeState.HomeRefreshAction:
homeState.updateIsRefresh(action.isRefresh)
homeState.updatePageNumber(action.pageNumber)
case let action as HomeState.HomeArticleResultAction:
switch action.result {
case .Success(let articleList):
homeState.updateArticleList(articleList.articleModels)
homeState.updateErrorMessage(nil)
case .Failure(let error):
switch error {
case .ResponseError(let qiitaError as QiitaError):
homeState.updateErrorMessage(qiitaError.message)
default:
homeState.updateErrorMessage("通信処理でエラーが発生しました")
}
}
case let action as HomeState.HomeMoreArticleResultAction:
if let moreArticleList = action.result.value {
homeState.appendArticleList(moreArticleList.articleModels)
}
case let action as HomeState.HomeShowMoreLoadingAction:
homeState.updateShowMoreLoading(action.showMoreLoading)
default:
break
}
newState.home = homeState
return newState
}
渡されたActionをどのActionなのか識別して、HomeStateを更新しています
HomeStateのupdateXX
メソッドは、HomeStateを更新する為のメソッドです
このメソッドはHomeStateが準拠していたArticleListScreenStateProtocol
に実装されているものです
実装を見ればわかりますが
homeState.updateIsRefresh(action.isRefresh)
は
homeState.isRefresh = action.isRefresh
と同じです
なぜ、ArticleListScreenStateProtocol
を作成したのかというのは後ほど説明するとして、HomeReducerが新たにアプリケーション全体のStateを生成し、Actionに応じてHomeStateを更新していることが分かったと思います
Store
State,Action,Reducerを設計したので、これでStoreを作ることができます
ReSwiftで提供されているStore
クラスのインスタンスを生成します
import UIKit
import ReSwift
let mainStore = Store(reducer: CombinedReducer([LoadingStateReducer(), HomeReducer(), ArticleDetailReducer(), UserArticleListReducer(), UserArticleDetailReducer()]), state: AppState())
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
第一引数のreducer
について、本サンプルアプリでは画面などの単位でStateを設計して、そのStateに対応するActionとReducerも分割して作ったので、分割して作成した複数のReducerをCombinedReducer構造体を使うことで、まとめています
第二引数のstate
には先ほど記載したAppStateを登録しています
ここまでの説明では、ホーム画面のみですが、AppStateの完成系は各画面のStateやアプリ全体に共通して使うStateがツリー構造で登録されています
import Foundation
import ReSwift
struct AppState: StateType {
var loading = LoadingState() //statusbarインジケータのState
var home = HomeState() //ホーム画面のState
var articleDetail = ArticleDetailState() //ホーム画面で選択した投稿の詳細画面のState
var userArticleList = UserArticleListState() //選択したユーザーの投稿一覧画面のState
var userArticleDetail = ArticleDetailState() //ユーザーの投稿一覧で選択した投稿の詳細画面のState
}
これでアプリケーション内で1つだけ存在し、1つのアプリケーションの状態(State)を保持するStoreができました
Storeはグローバル領域で定義されているので、どこからでもmainStore
でStoreにアクセス可能です
ReSwiftの処理の流れ
ReSwiftの処理の流れを追うためにホーム画面の実装ファイルであるArticleListTableViewController.swiftを見ながら説明します
リスナーの登録と解除
まずはStoreが保持しているStateが新しいものに変わった時に通知を検知できるようにStoreにリスナーを登録します
Storeのsubscribe
メソッドで登録できます
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
mainStore.subscribe(self)
}
リスナーの解除はunsubscribe
メソッドを呼び出します
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
mainStore.unsubscribe(self)
}
登録するリスナーは必ずStoreSubscriber
プロトコルに準拠していなければなりません
準拠するにはfunc newState(state: StoreSubscriberStateType)
メソッドを実装すれば良いです
引数の型であるStoreSubscriberStateType
型にはアプリケーション単一のStateであるAppStateを指定します
extension ArticleListTableViewController: StoreSubscriber {
func newState(state: AppState) {
//StoreのStateが新たなStateに変わった時に呼ばれる
}
}
これで、Storeが保持しているStateが新たに生まれ変わった際に検知できるようになりました
Actionの生成とdispatch
ホーム画面のデータをAPI通信して取得するActionをdispatchしているrefreshData
メソッドを見ていきます
func refreshData() {
mainStore.dispatch(LoadingState.LoadingAction(isLoading: true))
let refreshStartAction = HomeState.HomeRefreshAction(isRefresh: true, pageNumber: 1)
mainStore.dispatch(refreshStartAction)
let actionCreator = QiitaAPIActionCreator.call(generateAllArticleRequest()) { [weak self] result in
let pageNumber = self?.homeState.pageNumber ?? 1
let refreshEndAction = HomeState.HomeRefreshAction(isRefresh: false, pageNumber: pageNumber)
mainStore.dispatch(refreshEndAction)
mainStore.dispatch(LoadingState.LoadingAction(isLoading: false))
let action = HomeState.HomeArticleResultAction(result: result)
mainStore.dispatch(action)
}
mainStore.dispatch(actionCreator)
}
まず、LoadingState
を変更するLoadingAction
を発行してStoreにdispatchしていますが、これはホーム画面というより全画面で共有するStateなので一旦説明を割愛します
let refreshStartAction = HomeState.HomeRefreshAction(isRefresh: true, pageNumber: 1)
mainStore.dispatch(refreshStartAction)
次にHomeStateのisRefresh
とpageNumber
を変更するHomeRefreshAction
を生成してStoreにdispatchしています
StoreはActionが送られると、middlewareを介して登録されている全ReducerのhandleAction
メソッドを呼び出します
(発行されたActionと現在のStateを引数に渡して)
実際にHomeRefreshAction
がdispatchされた際にStateを変更するのはHomeReducerのswitch文の下記部分です
case let action as HomeState.HomeRefreshAction:
homeState.updateIsRefresh(action.isRefresh)
homeState.updatePageNumber(action.pageNumber)
HomeReducer以外の他のReducerではswitch文の条件を通過するだけです
Actionが全Reducerに送られ、そこで新しく生まれ変わったStateがStoreに保持されます
そしてStoreが登録されている各リスナーに新たなStateを通知します
すなわち、func newState(state: AppState)
が呼ばれるということです
引き続きArticleListTableViewControllerクラスのrefreshData
メソッドを見ていきます
let actionCreator = QiitaAPIActionCreator.call(generateAllArticleRequest()) { [weak self] result in
let pageNumber = self?.homeState.pageNumber ?? 1
let refreshEndAction = HomeState.HomeRefreshAction(isRefresh: false, pageNumber: pageNumber)
mainStore.dispatch(refreshEndAction)
mainStore.dispatch(LoadingState.LoadingAction(isLoading: false))
let action = HomeState.HomeArticleResultAction(result: result)
mainStore.dispatch(action)
}
mainStore.dispatch(actionCreator)
ここでは、ActionCreatorが登場します
ActionCreatorはその名前の通りActionを生成します
ReSwiftのActionCreatorは下記のようにStateとStoreを引数に受け取って、オプショナルなActionを返すクロージャとして宣言されています
public typealias ActionCreator = (state: State, store: Store) -> Action?
ActionCreatorをStoreにdispatchすると、Storeが受け取ったActionCreator(クロージャ)を実行して、返り値のActionをもとにStateを更新するようになっています
public func dispatch(actionCreatorProvider: ActionCreator) -> Any {
let action = actionCreatorProvider(state: state, store: self)
if let action = action {
dispatch(action)
}
return action
}
本サンプルコードの、QiitaAPIActionCreatorのcallメソッドの返り値がActionCreator
になっています
struct QiitaAPIActionCreator {
static func call<Request: QiitaRequestType>(request: Request, responseHandler: (Result<Request.Response, SessionTaskError>) -> Void) -> Store<AppState>.ActionCreator {
return { state, store in
Session.sendRequest(request) { result in
responseHandler(result)
}
return nil
}
}
....
}
このActionCreatorは汎用的にしたかったので、渡されたRequestへの通信処理を実行して、結果をコールバックに渡すだけで、Action自体は生成しません(常にnilを返します
通信処理が終わった時に呼び出し元であるコールバック側からActionを発行してStoreにdispatchするようにしています
ActionCreatorの役割としてこれで良いのか?という疑問は残りますが、ReSwiftのサンプルコードでnilのActionを返し、通信処理だけしているActionCreatorがいたので、とりあえずこれで良いかといった感じで作りました(;^_^
ReSwiftにAsyncActionCreatorなるものがあるのですが、それを使えば良かったのかもしれません
ホーム画面のActionCreatorのコールバック側で発行しているActionはHomeRefreshAction
,LoadingAction
,HomeArticleResultAction
です
let pageNumber = self?.homeState.pageNumber ?? 1
let refreshEndAction = HomeState.HomeRefreshAction(isRefresh: false, pageNumber: pageNumber)
mainStore.dispatch(refreshEndAction)
mainStore.dispatch(LoadingState.LoadingAction(isLoading: false))
let action = HomeState.HomeArticleResultAction(result: result)
mainStore.dispatch(action)
HomeRefreshAction
を生成してStoreにdispatch
LoadingAction
を生成してStoreにdispatch
HomeArticleResultAction
を生成してStoreにdispatch
しているので、それぞれのActionがReducerに処理されてStateが変わるたびにnewStateメソッドが呼び出されます
すなわち、Actionをdispatchした回数である3回、newStateメソッドが呼び出されるわけです
このデータフローを踏まえ、newStateメソッドではホーム画面に必要なStateの状態の組み合わせを考慮して処理を書く必要があります
ちなみに、サンプルコード内で使っているhomeState
プロパティはmainStore.state.home
と毎度書くのが冗長に感じたので、
private var homeState: HomeState {
return mainStore.state.home
}
として簡潔な名前でアクセスできるようにしているだけのGetterです
ホーム画面のnewState
メソッドは下記のよう実装しています
func newState(state: AppState) {
if homeState.hasError() {
showErrorDialog()
return
}
if homeState.isRefresh && homeState.pageNumber == 1 {
expireCache()
}
reloadView()
}
簡単に何をしているか説明すると
-
homeState
が通信エラーの文字列が保持している場合はエラーダイアログを出して処理終了 - 初回データ取得状態(引っ張って更新含む)の場合は画像のキャッシュデータ削除
-
homeState
の次の◯件読み込み中フラグに応じて、最下部Viewのインジケーターの表示制御 -
homeState
が投稿データを1件以上保持していて、初回データ取得状態(引っ張って更新含む)でない場合はTableViewのレンダリングなどをする
といった処理をしています
この処理で、きちんと通信処理が成功した場合にViewのレンダリングを走らせるには、HomeStateのerrorMessage
プロパティがnilでなければなりません
どこでnilにしているかというとReducerでやっています
通信処理の結果をもとに生成されるHomeArticleResultActionは、Result<GetAllArticleEndpoint.Response, SessionTaskError>
のプロパティを格納したActionなのでReducerが成功or失敗を判定して、成功パターンの場合はHomeStateのarticleList
にデータを入れて、errorMessage
はnilにしています
case let action as HomeState.HomeArticleResultAction:
switch action.result {
case .Success(let articleList):
homeState.updateArticleList(articleList.articleModels)
homeState.updateErrorMessage(nil)
case .Failure(let error):
switch error {
case .ResponseError(let qiitaError as QiitaError):
homeState.updateErrorMessage(qiitaError.message)
default:
homeState.updateErrorMessage("通信処理でエラーが発生しました")
}
}
もしかするとReducerで判定するのではなく、ActionCreatorのコールバックの中で成功or失敗を判定して、HomeStateのarticleList
の変更専用のActionとerrorMessage
を変更する専用のActionに分けてあげるべきだったのでは?と思ったりしています
ReSwiftのデータフローを踏まえて意識・注意したこと
注意点として、ActionをStoreにdispatchするたびにStateが変わりnewState
メソッドが呼ばれるので当然、dispatchするActionの順番が変われば、newState
メソッドが呼び出された時のStateが違うので、きちんと考慮して無駄な処理が走らないようにを心がけました
例として、
- ホーム画面では
newState
メソッド内の処理で初回データ取得状態(引っ張って更新含む)の場合は画像のキャッシュデータ削除
という条件があります
その条件を満たす状態にするにはHomeState.HomeRefreshAction(isRefresh: true, pageNumber: 1)
というActionを発行してStoreにdispatchすれば良いです
HomeState.HomeRefreshAction(isRefresh: true, pageNumber: 1)
をdispatchしてnewState
が呼ばれてキャッシュが削除されます
次にStatusBarのインジケーター表示制御のStateを変えるAction LoadingState.LoadingAction(isLoading: true)
を発行した場合、HomeStateは先ほどと同じく、キャッシュ削除の条件を満たす状態なのでホーム画面のnewState
が呼ばれた時に、再び画像のキャッシュを削除してしまいます
なので、HomeState.HomeRefreshAction(isRefresh: true, pageNumber: 1)
よりも先にLoadingState.LoadingAction(isLoading: true)
をdispatchすべきですし、コールバックでdispatchするActionもまずはキャッシュ削除の条件を満たさない状態にするActionであるHomeState.HomeRefreshAction(isRefresh: false, pageNumber: pageNumber)
をdispatchしてから後続のActionを発行してdispatchするべきです
また、viewWillAppear
でmainStore.subscribe(self)
をしてviewDidDisappear
でmainStore.unsubscribe(self)
をしていますが、これはホーム画面から詳細画面に遷移して、詳細画面のViewControllerが自身の対象となるStateを変えるためにActionを発行して、Storeにdispatchするたびに現在アクティブ表示でない、ホーム画面のViewControllerのnewState
メソッドも常に呼ばれるので、無駄にViewのレンダリングなどの処理が走ることになるのを避けています
ちなみにviewWillAppear
でmainStore.subscribe(self)
を呼び出した瞬間にStoreに登録されている全てのリスナーのnewState
メソッドが呼ばれるようになっています
ReSwiftを使う上で悩んだところ
ReSwiftを使って実装する上で一番悩んだのは、ViewやViewControllerを共通で使いたい場合にどうすれば良いのかということです
例えばこのサンプルアプリだとホーム画面とユーザーの投稿一覧画面はViewの構造もViewに食わせるデータ構造も同じです
違いは、食わせるデータがユーザー固有のものに絞られているかというだけです
(投稿詳細画面も同じですが今回は一覧をサンプルとして出します)
ホーム(投稿一覧) | ユーザー投稿一覧 |
---|---|
ReSwiftを使っていない場合は、投稿一覧取得APIのRequestにuserIdのパラメータを指定するかしないかの違いだけなので、簡単に対応できそうです
仮にViewのデータ構造が全く同じで、Viewに食わせるデータ構造だけ違う場合の画面であったとしても、Viewに食わせるモデルのインターフェースを共通化するProtocolに準拠させることで解決できます
私が、ReSwiftの場合に悩んだのは、
Stateは使いまわせても(別インスタンスを作ってAppStateに登録すれば良い)、どのStateを変えるかというActionを共通化できなかったところです
例えば、ホーム画面とユーザー投稿一覧画面で同一インスタンスのStateを監視していたら、ユーザー投稿一覧画面に遷移してから、ホーム画面に戻ってくるとホーム画面の表示が先ほど表示したユーザー投稿一覧画面と同じ内容のものになっていまいます
なので当然ですが、同一のStateを監視するのは、アプリ内(画面間)で共有したいStateの場合だけです
変更させるべき対象のStateに対するActionを動的に生成してうまいことコントロールする層を作る必要があるんだろうなと感じました
今回のサンプルアプリでは 面倒臭くなって 同じViewControllerを書いて、Actionを発行するところを変えるというイケテナイ実装になっています
最低でもViewController継承してAction発行する箇所をメソッドに切り出してoverrideすべきだったと反省
(しかしStateやActionが増えるごとにViewやViewControllerを継承するのか!?)
せめてViewに食わせたりする部分は共通化しようと思って苦肉の策で作ったのが、HomeStateが準拠していたArticleListScreenStateProtocol
なのです
ReSwiftを使ってみて感想
- グローバルな領域にアプリの全ての状態を持つことに対して、どこからでもActionを発行して他の画面とか関係ないところの状態を変えることができるというのは、理論上はやらないと分かりつつも精神衛生上、不安な気持ちになった
- アプリ間で共有するようなところだけグローバルで持てればなとか思った
- アプリ間、画面間で共有したい状態をグローバルで持つので同期が楽
- 状態を持つ場所が明確であり、状態を変更する際の流れが一方行になることで曖昧さがなくなるのは凄く良い
- 状態変化が起きた際の処理を一箇所にまとめることができるので可読性が良いし処理が散らばらないのでデバッグしやすい
- 状態の再現やテストがしやすい
- レンダリングコストとかあまり気にする必要ないかもだけれど、Stateのどこが更新されたか差分を取得できる仕組み作ると更に良さそう
- 沢山の状態を持ち、その状態が複雑に絡み合うような大きなアプリで採用するには良さそうだなと感じた
- ホーム画面とかでPullToRefreshした時に
refreshData
メソッドを直接呼んじゃってるけど、実際にPullToRefreshしてるかの状態をStateに持たせてActionをdispatchしてあげるべきなんだろうな
今回やれていないこと
- middlewareの活用
- ReSwiftリポジトリのデモで使われているReSwift-RouterやReSwift-Recorderが、どんなことを解決できるか調べていない
最後に
本当に簡単なサンプルアプリを作成した程度なのでReSwiftの良し悪しを語れるレベルにはないと思っています
そもそも私の作成したサンプルアプリではStateの設計とかReduxの概念に沿ってちゃんと作れているのか些か不安が残る
興味のある方は実際に触ってみると面白いと思います
ReSwiftの表面的な使い方としてお役に立てれば幸いです