Actionを使って快適なViewModel生活を🏄
アプリを開発してる上でAPI通信は欠かせませんが、RxSwiftを使って開発している場合
API通信で起こるエラーを正しく処理しなくてはなりません!
Observable
の代わりにDriver
を使うのも一手ですが、オペレーターが充実していないので困ることもあるかと思います。
今回はActionというライブラリーを使ってAPI通信・エラーの処理を行う方法を書きます。
RxSwiftで実装した場合
例えばViewModelでAPIからFoo
の配列を取得する場合、まずは以下のように書くことができる。
class FooViewModel {
let refreshTrigger = PublishSubject<Void>()
let foos = PublishSubject<[Foo]>()
init() {
refreshTrigger
.flatMap { APIClient.shared.responseFrom(request) }
.bindTo(foos)
.addDisposeBag(disposeBag)
}
}
しかしこのコードだと以下のような問題があります
- Responseがエラーだった場合に
foos
に.error
が流れてしまいUIのバインディングが切れてしまう。 -
foos
のプロパティーがViewController側からアクセスできてしまう。(ReadOnlyにするべき)
上記の問題を解決するには以下のように変更することができます。
class FooViewModel {
let refreshTrigger = PublishSubject<Void>()
var foos: Observable<[Foo]> {
return foosSubject
}
var error: Observable<Error> {
return errorSubject
}
private let foosSubject = PublishSubject<[Foo]>()
private let errorSubject = PublishSubject<Error>()
init() {
refreshTrigger
.flatMap { [weak self] _ in
return APIClient.shared.responseFrom(request)
.catchError { _ in .empty } // catchErrorJustReturn([])も可能
.do(onError: { [weak self] error in
self?.errorSubject.onNext(error)
})
}
.bindTo(foosSubject)
.addDisposeBag(disposeBag)
}
}
- Responseが
.error
の場合に.empty
を返してストリームを維持する - ViewModelからのアウトプットをComputed Propertyにして外部から変更できないようにする。
- エラーをViewControllerで処理できるようにする。
RxSwiftを使って開発している場合、ViewModelは大体こんな感じになっていると思う!
Actionを導入する🏄
Actionってなに??
- Githubのレポジトリーはこちら
ReadMeにはこのように書かれています
An action is a way to say "hey, later I'll need you to subscribe to this thing." It's actually a lot more >involved than that.
Actions accept a workFactory: a closure that takes some input and produces an observable. When execute() is called, it passes its parameter to this closure and subscribes to the work.
- Can only be executed while "enabled" (true if unspecified).
- Only execute one thing at a time.
- Aggregates next/error events across individual executions.
まとめるとこんな感じ
- トリガーとなる
Input
を受け取り、workFactory
というクロージャーを実行し、Output
をObservable
で吐き出す。 - 一度に実行できるのは1個だけ(直列処理)
-
.next
と.error
を別々に扱う
Actionの実装について
Actionの中には以下のようなプロパティーが用意されている。
public final class Action<Input, Element> {
public typealias WorkFactory = (Input) -> Observable<Element>
public let workFactory: WorkFactory
public let inputs = PublishSubject<Input>()
public let errors: Observable<ActionError>
public let elements: Observable<Element>
}
実行される順序
-
inputs
をonNext
するのをトリガーにworkFactory
が実行される -
workFactory
の結果がelements
に流れてくる(これがアウトプット) - もし
workFactory
の中で.error
が起きたらerrors
に流れる。
つまりどういうことができるのか!👀
Actionを使って上のViewModelを書き換えると以下のように書くことができます!
class FooViewModel {
let refreshTrigger = PublishSubject<Void>()
var foos: Observable<[Foo]> {
return refreshAction.elements
}
var error: Observable<Error> {
return refreshAction.errors
.flatMap { actionError -> Observable<Error> in
if case .underlayingError(let error) = actionError {
return Observable.of(error)
} else {
return .empty()
}
}
}
private let refreshAction = Action<Void, [Foo]> { _ in
return APIClient.shared.responseFrom(request)
}
init() {
refreshTrigger
.bindTo(refreshAction.inputs)
.addDisposeBag(disposeBag)
}
}
-
refreshAction
のInput
はVoid
なのでrefreshTrigger
をaction
のinputs
にバインドできる。 -
refreshTrigger
がonNext
されたタイミングでaction
内でAPI通信が行われる。 - Responseが指定した型(この場合は
[Foo]
)でelements
に流れてくるので、そのままComputed Propertyの値に使う。 - Errorの場合はactionの
errors
に流れてくるので、これもそのままComputed Propertyの値に使う。
まとめ
Actionを使うことによって
- APIの処理を切り出して行うことができる。
- Errorが流れてしまってUIのバインディングが切れることはない。
- ViewModelからのアウトプットをComputed Propertyで定義する時に
PublishSubject
などを経由しなくて良い。
是非一度レポジトリーをのぞいてみてください🚀