[RxSwift]開発の効率を上げる便利なカスタムオペレータ

  • 8
    いいね
  • 0
    コメント

この記事ではカスタムオペレータ、つまり独自のオペレータを定義してRxSwiftをもっと便利に使う方法についてご紹介します。

一応最初に言っておくと、RxSwiftで利用できる標準のオペレータは高度に最適化・チューニングされたものなので、基本的には標準のオペレータを利用することが推奨されています。例えばmapなどと同じ働きをするオペレータを自分で実装することは簡単ですが、最初からRxSwiftに用意されているmapと同等の性能で動作するものを作ることは簡単ではありません。(参考 RxSwift Getting Started - Custom operators)

しかしながら、よく使う標準オペレータの組み合わせや、頻出するアプリ固有のロジックをカスタムオペレータとして定義しておいて、メソッドチェインで使うことで、コードの見通しをよくする、というのはそれほどデメリットもなく、開発の効率をあげてくれるのでは、と思います。

そこで、この記事では使ってみて便利だった3つのカスタムオペレータを紹介しつつ、その実装について説明してみます。

timeout - 一定期間値をemitしなかったらerrorにしたい

使い方

非常に時間がかかるかもしれない非同期処理の手続きがまとめられたloadAllUsersというObservableがあったとします。このような時間のかかる処理を伴ったObservableを購読したときに、もしも一定時間経過しても値をemitしなかった場合にはエラーにしたい、というようなケースは割とあります。

そんなとき、timeoutオペレータを利用すると下記のように記述することができます。

loadAllUsers
    .timeout(10, error: LoadUserError.timeOut) // 10秒経過すると指定したErrorがemitされる
    .subscribe{observer in
        // observerを見て、成功時/エラー時の処理を実装
    }
    .addDisposableTo(disposeBag)

実装

このtimeoutオペレータは、下記のようなシンプルな実装になっています。

Observable+Timeout.swift
extension Observable {
    public func timeout(_ interval: RxTimeInterval, error:Error, scheduler:SchedulerType = MainScheduler.instance) -> Observable {
        return amb(Observable.error(error).delaySubscription(interval, scheduler: scheduler))
    }
}

このtimeout関数はObservableのextensionとして実装されているのは、既存のObservableに対してメソッドチェインで.timeoutというように記述したいからです(Observableを引数に取ってObservableを返す関数を使うよりも.でチェインして書きたいので、、)。

それでは、メソッド自体の実装を具体的に見ていきましょう。ポイントはambdelaySubscriptionです。

amb

まずambの方からどういう働きをするオペレータなのか見てみましょう。このオペレータは一言で言うと「複数のObservableの中から、最初に値をストリームに流した方のObservableを選択する」という働きをします。

例えば、usagikameという2つのObservableがあってusagi.amb(kame)というコードを書いた場合、usagikameどちらの値が先に流れてくるか競争が開始されるイメージです。これを購読すると、先に値をストリームに流したObservableのみが選択され、その選ばれたObservableの値のみが流れてくる、という動きになります。usagiがわずかでも先に値をストリームに流すと、subscribeしたときにはusagiがemitする値のみが購読され、kameのemitする値は全て破棄されます。シビアな世界ですね。(参考: ambのマーブルダイアログ )

delaySubscription

では、次にdelaySubscriptionの方を見てみましょう。delaySubscriptionの挙動はシンプルで、その名の通り購読の開始を指定したinterval分遅らせるオペレータです。

注意する点としては、値がemitするのを遅延するのではなく、購読を遅延させる、という点です。(参考: RxSwiftのdelay/delaySubscription )

timeoutメソッドの動き

では、さきほどのambと合わせて処理を追ってみましょう。

まずErrorをemitするObservableをおもむろに生成します。しかし、Errorを抱えたObservableはdelaySubscriptionによって購読が遅延されるため、subscribeされたとしてもintervalで指定した期間の間は値が流れてくることがありません。そして、この「ErrorのObservable(interval経過後に値が購読される)」と「timeoutさせたい元のObservable(先ほどの例でいうとloadAllUsers)」の2つがambによる「どちらが値を先にストリームに流すか競争」の対象となります。先に値をストリームに流した方のObservableだけが、その後も値を流し続ける権利を得ることができるのです。

結果、intervalの時間内に元のObservableが値を発行したならば、通常どおり元のObserableの値がストリームに流れるし、もしもinterval分の時間が経過してしまったならば、Errorを流すObservableがambにより選択され、timeout関数で指定したErrorのみがストリームに流れるということになります。

このambdelaySubscriptionの挙動を頭の中で整理しながら毎回コードを書いたり読んだりするのは少し大変ですが、この処理をtimeoutというオペレータにすることで、可読性が上がり、再利用もしやすくなっていることがわかると思います。

なお、この実装はRxSwiftのIssuesに投稿されていたkzaherさんの実装を参考にしています。

doOnSubscribed(Disposed) - subscribe時/dispose時に処理を実行したい

あるObservableの購読が開始されたタイミング、あるいは破棄されたタイミングで何か処理をしたい、という欲求は割とあると思います。そんなときに役立つのがこのdoOnSubscribe/onDisposeメソッドです。ちなみにRxJavaには標準のオペレータとして同名のdoOnSubscribedがあるようです。

使い方

例えばloadAllUsersというユーザー情報を読み込む非同期処理がまとめられたObservableがあって、その開始と終了をハンドリングしたい、という場合には下記のように記述することができます。

loadAllUsers
    .do(onSubscribed:{ print("load始まったよ") })
    .do(onDisposed:{ print("load終わったよ") })

特定の処理が開始/終了されるタイミングで、フラグや状態をハンドリングする必要があるといった場合に非常に便利なオペレータです。

実装

実装はシンプルで、usingを利用しています。

Observable+DoOnSubscribed.swift
extension Observable {
    public func `do`(onSubscribed: @escaping  () -> () = {}, onDisposed: @escaping () -> () = {}) -> Observable {
        return Observable.using({
            onSubscribed()
            return Disposables.create(with: onDisposed)
        }, observableFactory: { _ -> Observable in
            return self
        })
    }
}

usingは、Observableと同じ期間の生存期間を持つリソースを生成することができるオペレータです。

Observable.usingの1つ目の引数となるクロージャでObservableの購読が開始されたときに実施する処理を記述し、さらに返り値としてObservableの購読が終了した時点で実施される処理を記述したDisposableを返します。このクロージャの中で、リソースとして利用したいオブジェクトを生成しておけば、そのリソースはあるObservableが購読される期間と同じだけ生存し、かつ初期化と破棄の処理もこのクロージャに実装することが出来るわけです。Observable.usingの2つ目の引数は、リソースを紐付ける対象となるObservableを生成するためのクロージャです。

詳しい挙動についてはRxSwiftの機能カタログにあるIndicatorの例がわかりやすいです。

インターフェイスを見ての通り、直感的にはわかりづらい部分があるのと、メソッドチェインで記述することができないという点が悲しいので、usingを直接使うよりはこのdoOnSubscribeの例のように必要に応じてusingを内部で使ったObservableの拡張を用意することの方が多いかな、という気がします。

例えばよくあるアプリ独自のローディングを表示する、などといったケースもusingを利用したカスタムオペレータを作っておくと便利だと思います。

注意点

実は、上記のコードはRxSwift3.0 + Xcode8.1ではコンパイルエラーとなってしまいました。文法としては間違ってないように思えますが、どうやらusingを利用したときに型の推論が正しく機能しない、というケースがあるようです。これについては @mono0926 さんが見つけてくれたworkaroundsを改変したものを利用して回避することができました。

RxSwift3.0 + Xcode8.1で実際にコンパイルが通るようになったコードは下記のような感じです。

Observable+DoOnSubscribed.swift
struct DisposableWrapper: Disposable {
    private let disposable: Disposable
    init(disposable: Disposable) {
        self.disposable = disposable
    }
    func dispose() {
        disposable.dispose()
    }
}

extension Observable {
    public func `do`(onSubscribed: @escaping  () -> () = {}, onDisposed: @escaping () -> () = {}) -> Observable {
        return Observable.using({
            return self.wrapMethod(onSubscribed: onSubscribed, onDisposed: onDisposed)
        }, observableFactory: { _ -> Observable in
            return self
        })
    }

    private func wrapMethod(onSubscribed: @escaping () -> (), onDisposed: @escaping () -> ()) -> DisposableWrapper {
        onSubscribed()
        return DisposableWrapper(disposable: Disposables.create(with: onDisposed))
    }
}

もっとスマートな回避方法あれば、ぜひ教えてください :pray: なお、このdoOnSubscribedの実装もやはりRxSwiftのIssuesに投稿されていたkzaherさんの実装を参考にしています。

negate - Boolを逆にする

最後に紹介するのは、ElementのBoo値を逆にするシンプルなカスタムオペレータです。例えばisInvalidというObservableがあって、値がtrueのときのみ警告を表示するViewを表示したい、というような場合、普通に書くと下記のようになります。

Observable+Negate.swift
isInvalid
    .map{!$0}
    .bindTo(invalidMessageLabel.rx.isHidden)
    .addDisposableTo(disposeBag)

これでも悪くはないのですが、 .map{!$0} を何度も書くと何となく心が濁ります。そこで、ElementのBool値を逆にするだけのオペレータ、negateを使うと下記のようになります。

Observable+Negate.swift
isInvalid
    .negate
    .bindTo(invalidMessageLabel.rx.isHidden)
    .addDisposableTo(disposeBag)

タイプ数はほぼ変わらないですが、説明的な名前のついたComputedPropertyを利用することで、いくぶんか心を安寧を得ることができました。

実装

このnegateの実装は極めてシンプルで、下記の4行だけです。

Observable+Negate.swift
extension ObservableType where E == Bool {
    var negate:Observable<Bool> {
        return self.map(!)
    }
}

map(!) というのが非常にオシャレですね。 ここでのmapの引数は、Bool型の引数をとってBool型の値を返す関数です。 ! の定義を見ると、、

prefix public static func !(a: Bool) -> Bool

これはまさしく今mapに渡そうとしている関数なので、 map(!) とだけ書けば良いというわけです。このオシャレなnegateの実装はCustom convenience operators with RxSwiftという記事を参考にさせて頂きました。

おわりに

以上、簡単にではありますがRxSwiftの便利なカスタムオペレータのご紹介でした。他にもっと便利なカスタムオペレータはないか、、という方はRxSwiftCommunityのリポジトリを巡ってみたり、RxSwiftのIssuesを眺めたりすると、便利なオペレータを含むライブラリを見つけたり、オペレータの実装を閃いたりするのでは、と思います。

さて。2016年はRxSwiftが各所で話題になりましたが、2017年にはさらに定着して実プロダクトでの利用例もますます増えて行きそうです。というわけで、来年もみんなでRxSwiftやSwiftの色んな知見を共有していきましましょう。