LoginSignup
156
132

More than 5 years have passed since last update.

SwiftでReduxを実現するReSwiftを使ってみた

Posted at

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つで下記の画面が存在します

ホーム(投稿一覧) 投稿詳細 ユーザー投稿一覧 ユーザー投稿詳細
Simulator Screen Shot 2016.06.20 21.44.14.png Simulator Screen Shot 2016.06.20 21.44.19.png Simulator Screen Shot 2016.06.20 21.44.24.png Simulator Screen Shot 2016.06.20 21.44.37.png

画面ごとの要件や構成

  • ホーム画面(投稿一覧)
    • Qiitaの全投稿一覧が表示される
    • 一度に読み込まれる投稿は20件
    • 最下部までスクロールすると次の20件を読み込む
    • セルをタップすると、タップした投稿の詳細画面へ遷移する
  • 投稿詳細画面
    • 投稿の詳細が表示される
    • ユーザー名のリンクをタップすると、そのユーザーの投稿一覧画面に遷移する
    • ストックボタンをタップすると投稿をストックor解除できる
    • 本文はWebViewで表示されている
  • ユーザー投稿一覧画面
    • 選択したユーザーの投稿一覧が表示される
    • あとはホーム画面(投稿一覧)と同じ
  • ユーザー投稿詳細
    • 投稿詳細画面と同じだが、ユーザー名のリンクは存在しない

今回作成したサンプルアプリの動かし方

とりあえずサンプルコード見て理解したいという方はここに今回作成したサンプルコードがあります

ビルドが通るようになる一連の作業としては

git clone git@github.com:hachinobu/ReduxSwiftQiitaClient.git

でcloneして、cd ReduxSwiftQiitaClientでプロジェクト内に移動してください
その後にpod installを叩いてライブラリを入れてください

また、サンプルアプリを動かすにはQiitaのアクセストークンが必要です
アクセストークンは、この取得画面から発行できます
スコープは下記スクリーンショットの通り、read_qiitawrite_qiitaにチェックをつけて「発行する」ボタンから発行してください

スクリーンショット 2016-06-20 22.41.11.png

アクセストークンが発行されたら、それをコピーしてReduxSwiftQiitaClientプロジェクト内にSecrets.plistという名前のplistファイルを作成して
Dictionary TypeでAccessToken: 'Qiitaアクセストークン' となるようにしてください
(下記スクリーンショットのhogehoge部分を先ほど取得したアクセストークンに差し替えてください)

スクリーンショット 2016-06-20 22.44.52.png

これでビルドして成功すればアプリを使うことができます

ReSwiftを使った実装(サンプルアプリ)の説明

ReSwiftは当然Reduxの概念に基づいているのでStore,State,Reducer,Actionが存在します

アプリケーションの状態(State)を保持するStoreを生成する必要がありますが、StoreはStateとReducerを先に作っておかないと生成できないので、まずはホーム画面を例にしてStateを設計してみます

Simulator Screen Shot 2016.06.20 21.44.14.png

ホーム画面のState

ホーム画面の表示項目に必要なものやAPI通信などを考慮して設計してみました

HomeState.swift
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群です

HomeStateAction.swift

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含む)開始/終了時に発行
  • HomeShowMoreLoadingAction

    • HomeStateのarticleListもしくはerrorMessageを変更する情報を持つAction
    • Result型の投稿一覧取得APIの結果(成功or失敗)情報が格納される
    • 投稿一覧APIのレスポンスを受け取った際に発行
  • HomeShowMoreLoadingAction

    • HomeStateのshowMoreLoadingを変更する情報を持つAction
    • ページング処理(次の◯件読み込み)の開始/終了時に発行
  • HomeMoreArticleResultAction

    • HomeStateのarticleListに追加するデータを持つAction
    • ページング処理(次の◯件読み込み)の◯ページ目の投稿一覧APIのレスポンスを受け取った際に発行

ホーム画面のReducer

次に現在のStateとActionをもとに新たなStateを生成するReducerです
ReducerはReSwiftのReducerプロトコルに準拠する必要あります

Reducer.swift
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です

HomeReducer.swift
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だけ作った現在だと下記です

AppState.swift
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クラスのインスタンスを生成します

AppDelegate.swift
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がツリー構造で登録されています

AppState.swift

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メソッドで登録できます

ArticleListTableViewController.swift
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        mainStore.subscribe(self)
    }

リスナーの解除はunsubscribeメソッドを呼び出します

ArticleListTableViewController.swift
    override func viewDidDisappear(animated: Bool) {
        super.viewDidDisappear(animated)
        mainStore.unsubscribe(self)
    }

登録するリスナーは必ずStoreSubscriberプロトコルに準拠していなければなりません
準拠するにはfunc newState(state: StoreSubscriberStateType)メソッドを実装すれば良いです
引数の型であるStoreSubscriberStateType型にはアプリケーション単一のStateであるAppStateを指定します

ArticleListTableViewController.swift
extension ArticleListTableViewController: StoreSubscriber {

    func newState(state: AppState) {
        //StoreのStateが新たなStateに変わった時に呼ばれる
    }

}

これで、Storeが保持しているStateが新たに生まれ変わった際に検知できるようになりました

Actionの生成とdispatch

ホーム画面のデータをAPI通信して取得するActionをdispatchしているrefreshDataメソッドを見ていきます

ArticleListTableViewController.swift
    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なので一旦説明を割愛します

ArticleListTableViewController.swift
let refreshStartAction = HomeState.HomeRefreshAction(isRefresh: true, pageNumber: 1)
mainStore.dispatch(refreshStartAction)

次にHomeStateのisRefreshpageNumberを変更するHomeRefreshActionを生成してStoreにdispatchしています

StoreはActionが送られると、middlewareを介して登録されている全ReducerのhandleActionメソッドを呼び出します
(発行されたActionと現在のStateを引数に渡して)

実際にHomeRefreshActionがdispatchされた際にStateを変更するのはHomeReducerのswitch文の下記部分です

HomeReducer.swift
    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メソッドを見ていきます

ArticleListTableViewController.swift
    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を更新するようになっています

Store.swift
    public func dispatch(actionCreatorProvider: ActionCreator) -> Any {
        let action = actionCreatorProvider(state: state, store: self)

        if let action = action {
            dispatch(action)
        }

        return action
    }

本サンプルコードの、QiitaAPIActionCreatorのcallメソッドの返り値がActionCreatorになっています

QiitaAPIActionCreator.swift
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と毎度書くのが冗長に感じたので、

ArticleListTableViewController.swift
    private var homeState: HomeState {
        return mainStore.state.home
    }

として簡潔な名前でアクセスできるようにしているだけのGetterです

ホーム画面のnewStateメソッドは下記のよう実装しています

ArticleListTableViewController.swift

    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にしています

HomeReducer.swift
    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するべきです

また、viewWillAppearmainStore.subscribe(self)をしてviewDidDisappearmainStore.unsubscribe(self)をしていますが、これはホーム画面から詳細画面に遷移して、詳細画面のViewControllerが自身の対象となるStateを変えるためにActionを発行して、Storeにdispatchするたびに現在アクティブ表示でない、ホーム画面のViewControllerのnewStateメソッドも常に呼ばれるので、無駄にViewのレンダリングなどの処理が走ることになるのを避けています

ちなみにviewWillAppearmainStore.subscribe(self)を呼び出した瞬間にStoreに登録されている全てのリスナーのnewStateメソッドが呼ばれるようになっています

ReSwiftを使う上で悩んだところ

ReSwiftを使って実装する上で一番悩んだのは、ViewやViewControllerを共通で使いたい場合にどうすれば良いのかということです

例えばこのサンプルアプリだとホーム画面とユーザーの投稿一覧画面はViewの構造もViewに食わせるデータ構造も同じです
違いは、食わせるデータがユーザー固有のものに絞られているかというだけです
(投稿詳細画面も同じですが今回は一覧をサンプルとして出します)

ホーム(投稿一覧) ユーザー投稿一覧
Simulator Screen Shot 2016.06.20 21.44.14.png Simulator Screen Shot 2016.06.20 21.44.24.png

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-RouterReSwift-Recorderが、どんなことを解決できるか調べていない

最後に

本当に簡単なサンプルアプリを作成した程度なのでReSwiftの良し悪しを語れるレベルにはないと思っています
そもそも私の作成したサンプルアプリではStateの設計とかReduxの概念に沿ってちゃんと作れているのか些か不安が残る

興味のある方は実際に触ってみると面白いと思います

ReSwiftの表面的な使い方としてお役に立てれば幸いです

156
132
3

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
156
132