Posted at
RxSwiftDay 24

Actionを使って快適なViewModel生活を🏄

More than 1 year has passed since last update.


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ってなに??


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というクロージャーを実行し、OutputObservableで吐き出す。

  • 一度に実行できるのは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>
}


実行される順序



  1. inputsonNextするのをトリガーにworkFactoryが実行される


  2. workFactoryの結果がelementsに流れてくる(これがアウトプット)

  3. もし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)
}
}



  • refreshActionInputVoidなのでrefreshTriggeractioninputsにバインドできる。


  • refreshTriggeronNextされたタイミングでaction内でAPI通信が行われる。

  • Responseが指定した型(この場合は[Foo])でelementsに流れてくるので、そのままComputed Propertyの値に使う。

  • Errorの場合はactionのerrorsに流れてくるので、これもそのままComputed Propertyの値に使う。


まとめ


Actionを使うことによって


  • APIの処理を切り出して行うことができる。

  • Errorが流れてしまってUIのバインディングが切れることはない。

  • ViewModelからのアウトプットをComputed Propertyで定義する時にPublishSubjectなどを経由しなくて良い。

是非一度レポジトリーをのぞいてみてください🚀