こんにちは。モバイル開発チームに所属している小島です。
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回しか実行されないことがわかると思います。また、実行時に UIActivityIndicator のアニメーションが動き、終わったら止まるといったことも実現できています。
Rx はパズルのように組み合わせて、いろんな機能を実現できるので楽しいですね。それでは。