RxSwift で多重実行防止と実行中の表現を簡潔に書く

こんにちは。モバイル開発チームに所属している小島です。

Rx を使うとコードをシンプルに書くことができるので好きです。今回はある処理(API呼び出しなどを想定)を同時に複数回実行しないような制限を実現する Extension を考えてみたので紹介したいと思います。

ActivityIndicator

RxSwift のサンプルコードに ActvityIndicator というものがあります。 UIKit に入っている UIActivityIndicator とは全く関係ないので紛らわしいです😢

それはさておき、こいつは何をしてくれるかというと Observable を関連付けると内部のカウンタを +1 して、その Observable が dispose されると -1 してくれます。つまりある Observable が実行状態であれば ON で、終了(Disposeされている)状態であれば OFF というのを表現できます。これを使って実現したいと思います。

Observable を拡張して、flatMap(withLock:) を追加

こんな便利なクラスがあれば、あとはそこまで難しくありません。

extension ObservableType {
    func flatMap<O>(withLock: ActivityIndicator, _ selector: @escaping (Self.E) throws -> O) -> RxSwift.Observable<O.E> where O : ObservableConvertibleType {
        return self
            .withLatestFrom(withLock) { input, loading in
                return (input, loading)
            }
            .filter { (input, loading) in
                return !loading
            }
            .flatMap({ (input, loading) -> RxSwift.Observable<O.E> in
                return (try! selector(input)).trackActivity(withLock)
            })
    }

Observable に対して、flatMap(withLock:) という関数を拡張します。これは何をやっているかというと、先の ActivityIndicator を引数にとって、ActivityIndicator が実行状態であれば処理をスキップし、終了状態であれば引数に渡したクロージャーを実行します。クロージャーは Observable を返すことになっていて、ActivityIndicator と関連付けをします。

こうすることで、返ってきた Observable が実行状態であれば flatMap(withLock:) を呼び出した場合には処理がスキップされます。

サンプルコード

文章で書いても伝わりづらいと思うので、サンプルコードも作ってみました。

このサンプルコードでは、doLongtimeTask という関数が定義されていて、処理が終わったら Complete する Observable を返します。(あと、確認のためにコールされたときにラベルにカウントを表示しています)

    private func doLongtimeTask() -> Observable<String> {
        self.callCount += 1
        self.callCountLabel.text = "`doLongtimeTask` called \(self.callCount) times."

        let asyncSubject = AsyncSubject<String>()

        let time = DispatchTime.now() + 5.0
        DispatchQueue.main.asyncAfter(deadline: time) {
            let result = "Complete! Endtime is \(Date().description)"
            asyncSubject.onNext(result)
            asyncSubject.onCompleted()
        }

        return asyncSubject.asObservable()
    }

また、ボタンタップ時の処理は以下のようにしています。flatMap(withLock:) に ActivityIndicator を渡すことで、doLongtimeTask がすでに実行中の場合は処理を実行しないようにできます。

        self.lockObject = ActivityIndicator()

        self.executeButton.rx.tap
            .flatMap(withLock: self.lockObject, { [weak self] _ in
                return self?.doLongtimeTask() ?? Observable.error(MyError.assertion)
            })
            .subscribe(onNext: { [weak self] result in
                self?.alert(message: result)
            })
            .disposed(by: self.disposeBag)

更に嬉しいのは、ActivityIndicator は現在実行中のものがあるかどうかを asObservable で取り出せますので、以下のように実行中に UIActivityIndicator (スピナーのほう!) のアニメーションを ON にするといったことも簡単にできます。

        self.lockState = self.lockObject.asObservable()

        self.lockState
            .bind(to: self.progressIndicator.rx.isAnimating)
            .disposed(by: self.disposeBag)

こんな感じでできます

EXECUTE ボタンを何度押しても、ちゃんと doLongtimeTask が1回しか実行されないこと表しているアニメーション

EXECUTE ボタンを何度押しても、ちゃんと doLongtimeTask が1回しか実行されないことがわかると思います。また、実行時に UIActivityIndicator のアニメーションが動き、終わったら止まるといったことも実現できています。

Rx はパズルのように組み合わせて、いろんな機能を実現できるので楽しいですね。それでは。