LoginSignup
53
32

More than 5 years have passed since last update.

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

Posted at

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などを経由しなくて良い。

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

53
32
0

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
53
32