subash
subash

Reputation: 182

How to detect if a observable has not emitted any events for specific time in RxSwift

I am trying to detect if a observable(my case button.rx.tap) has not emitted any value for say like 3 seconds. If yes, I would like to update the user interface. Here is my attempt so far:

Observable<Int>.interval(3, scheduler: MainScheduler.instance)
    .takeUntil(button.rx.tap) // I know take until will stop the timer sequence 
    .subscribe({ event in
        print(event)
        UIView.animate(withDuration: 0.4, animations: {
            if let number = event.element {
                let scale: CGFloat = number % 2 == 0 ? 1.5 : 1.0
                self.button.transform = CGAffineTransform(scaleX: scale, y: scale)
            }
        })
    })
    .addDisposableTo(disposeBag)

My goal is to animate the view whenever the button is not tapped for three seconds. I have tried scan, distinctUntilChanged and debounce but most combining operators I have encountered will emit items only when an item is emitted by a observable. Any help is much appreciated.

Upvotes: 4

Views: 7467

Answers (1)

tomahh
tomahh

Reputation: 13661

enum Event {
case tap
case timeout
}

let tapOrTimeout = button.rx.tap
  .map { _ in Event.tap }
  .timeout(3, scheduler: MainScheduler.instance)
  .catchErrorJustReturn(.timeout)

Observable.repeatElement(tapOrTimeout).concat()
  .subscribe(onNext: { event in
    switch event {
    case .tap: tapHandler()
    case .timeout: callForAttention()
    }
  })
  • timeout(_:scheduler:) will raise an error if no event comes out of the chain within 3 seconds.
  • catchErrorJustReturn(_:) will transform the error into a .timeout event
  • at this point, if the observable times out, it will also complete, hence nothing will happen afterward. Using Observable.repeatElement(_:).concat() we first create an Observable<Observable<Event>> and then concatenate inner observables. Which in our case means that we'll subscribe to the first, and the resubscribe to the same observable if the first completes.

If we had prefer to only play the callForAttention() animation once, we could have done the following

let tap = button.rx.tap
  .map { _ in Event.tap }

let tapOrTimeout = tap  
  .timeout(3, scheduler: MainScheduler.instance)
  .catchErrorJustReturn(.timeout)

Observable.of(tapOrTimeout, tap).concat()
  .subscribe(onNext: { /* same as above */})

This way, we first timeout, but then only emit an event when a tap occurs.

Upvotes: 10

Related Questions