こんにちは。モバイル開発チームに所属している小島です。
Rx を使うとコードをシンプルに書くことができるので好きです。今回はある処理(API呼び出しなどを想定)を同時に複数回実行しないような制限を実現する Extension を考えてみたので紹介したいと思います。
ActivityIndicator
RxSwift のサンプルコードに ActvityIndicatorというものがあります。 UIKit に入っている UIActivityIndicator とは全く関係ないので紛らわしいです😢
それはさておき、こいつは何をしてくれるかというと Observable を関連付けると内部のカウンタを +1 して、その Observable が dispose されると -1 してくれます。つまりある Observable が実行状態であれば ON で、終了(Disposeされている)状態であれば OFF というのを表現できます。これを使って実現したいと思います。
Observable を拡張して、flatMap(withLock:) を追加
こんな便利なクラスがあれば、あとはそこまで難しくありません。
extensionObservableType { funcflatMap<O>(withLock:ActivityIndicator, _ selector:@escaping (Self.E) throws ->O) ->RxSwift.Observable<O.E>where O :ObservableConvertibleType { returnself .withLatestFrom(withLock) { input, loading inreturn (input, loading) } .filter { (input, loading) inreturn!loading } .flatMap({ (input, loading) ->RxSwift.Observable<O.E>inreturn (try! selector(input)).trackActivity(withLock) }) }
Observable に対して、flatMap(withLock:) という関数を拡張します。これは何をやっているかというと、先の ActivityIndicator を引数にとって、ActivityIndicator が実行状態であれば処理をスキップし、終了状態であれば引数に渡したクロージャーを実行します。クロージャーは Observable を返すことになっていて、ActivityIndicator と関連付けをします。
こうすることで、返ってきた Observable が実行状態であれば flatMap(withLock:) を呼び出した場合には処理がスキップされます。
サンプルコード
文章で書いても伝わりづらいと思うので、サンプルコードも作ってみました。
このサンプルコードでは、doLongtimeTask という関数が定義されていて、処理が終わったら Complete する Observable を返します。(あと、確認のためにコールされたときにラベルにカウントを表示しています)
privatefuncdoLongtimeTask() ->Observable<String> { self.callCount +=1self.callCountLabel.text ="`doLongtimeTask` called \(self.callCount) times."letasyncSubject= AsyncSubject<String>() lettime= DispatchTime.now() +5.0 DispatchQueue.main.asyncAfter(deadline:time) { letresult="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] _ inreturnself?.doLongtimeTask() ?? Observable.error(MyError.assertion) }) .subscribe(onNext: { [weak self] result inself?.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 はパズルのように組み合わせて、いろんな機能を実現できるので楽しいですね。それでは。