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などを経由しなくて良い。
是非一度レポジトリーをのぞいてみてください🚀