タイトル通りタッチエンドからちょっと経ったらなにか処理をする+その間に次のタッチをされたらキャンセル、みたいなことをRx(Swift)でやりたかったが、UniRxからRxに入門した口なのでちょっと手間取った。

TL;DR. Observableを返すObservableに対してswitchLatestを使うとよい

実装

単にタッチエンドから一定時間経ったら、ということだったら普通にdebounceを使えばいいんだけど、その間に再びタッチされた場合にはキャンセルして、次のタッチエンドまでなにも流さないようにしたかった。

UniRxではRepeatとかを使っていたので(参考:「未来のプログラミング技術をUnityで -UniRx-」の例)、同じようにストリームが終了した時にまた最初からやり直すメソッドがあると思ったんだけど、Rxのrepeatは全然別の処理だった。 retryはエラー時にやり直すメソッドなのでまたちょっと違う。

でどうやるのか調べていたんだけど、結局全然違う方法を使う必要があって、Observableを入れ子にして最新のものだけを使用するというswitchLatestがそれだった。

let TOUCH_END_DURATION = 0.5

  public var strokeSettled: Observable<Bool> = PublishSubject<Bool>()
  private let resetTouchSubject = PublishSubject<()>()
  private var touchEndedSubject = PublishSubject<Bool>()

  required init(coder aDecoder: NSCoder) {
    ...
    strokeSettled = resetTouchSubject
      .map {[weak self] () -> Observable<Bool> in
        let subject = PublishSubject<Bool>()
        self?.touchEndedSubject = subject
        return subject
          .debounce(TOUCH_END_DURATION, scheduler: MainScheduler.instance)
      }
      .switchLatest()
      .share()
  }

  override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    resetTouchSubject.on(.Next())
  }

  override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    touchEndedSubject.on(.Next(false))
  }
  • strokeSettledが望みのストリームで、タッチ開始ごと(resetTouchSubject)に新しいObservabletouchEndedSubject)を流すストリームに対して、switchLatestで一番新しいObservableだけを対象にする
  • touchEndedSubjectのタッチエンドをdebounceでフィルタして流すことで、タッチから一定時間が経った時にイベントが流れる
  • タッチ開始ごとにswitchLatestで古いタッチエンドのObservableが破棄され新しい物に切り替えられるので、タッチエンドからすぐ次のタッチを開始した場合にはstrokeSettledにはイベントは流れない

参考

  • Intro to Rx - Combining sequencesにライブサーチの例が載っている。Mergeだとサーチ結果が混じってしまう危険性があるけど、Switchなら解決、と。