やりたいこと
RxSwiftを使ってタイマーを実現したい。
プロダクトでRxSwiftを導入しているので、Timer
クラスを使わずに、RxSwiftのinterval(_ period: RxTimeInterval, scheduler: SchedulerType)
で時間を計測しようと思った。
とりあえず実装
UISwitch
のON・OFFでタイマーのStopとStartを管理する簡単なSample
class ViewController: UIViewController {
@IBOutlet weak var timerLabel: UILabel!
@IBOutlet weak var timerSwitch: UISwitch!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
timerSwitch.rx.value
.flatMap { $0 ? Observable<Int>.interval(0.1, scheduler: MainScheduler.instance) : .empty() }
.map { String($0) }
.bind(to: timerLabel.rx.text)
.disposed(by: disposeBag)
}
}
結果
うまく動いていない。。。
switchをOFFにしてもtimerが止まらない。かつ、2度目のONで2つのTimerが動いているような挙動
とりあえずデバッグ
各イベントの後にdebug()
を仕込む
timerSwitch.rx.value
.debug("switch")
.flatMap { $0 ? Observable<Int>.interval(0.1, scheduler: MainScheduler.instance) : .empty() }
.debug("timer")
.map { String($0) }
.bind(to: timerLabel.rx.text)
.disposed(by: disposeBag)
デバッグ結果
2019-04-12 11:59:02.794: switch -> subscribed
2019-04-12 11:59:02.795: switch -> Event next(false)
2019-04-12 11:59:04.193: switch -> Event next(true) <- Switch On
2019-04-12 11:59:04.293: timer -> Event next(0)
2019-04-12 11:59:04.394: timer -> Event next(1)
2019-04-12 11:59:04.494: timer -> Event next(2)
2019-04-12 11:59:04.593: timer -> Event next(3)
2019-04-12 11:59:04.642: switch -> Event next(false) <- Switch Off
2019-04-12 11:59:04.693: timer -> Event next(4) <- あれ? 動いている
2019-04-12 11:59:04.794: timer -> Event next(5)
2019-04-12 11:59:04.893: timer -> Event next(6)
2019-04-12 11:59:04.994: timer -> Event next(7)
2019-04-12 11:59:05.248: switch -> Event next(true) <- Switch On
2019-04-12 11:59:05.293: timer -> Event next(8)
2019-04-12 11:59:05.349: timer -> Event next(0) <- 2つ目のTimerが起動
2019-04-12 11:59:05.394: timer -> Event next(9)
2019-04-12 11:59:05.449: timer -> Event next(1)
2019-04-12 11:59:05.493: timer -> Event next(10)
2019-04-12 11:59:05.550: timer -> Event next(2)
flatMap
内で生成したObservable.interval
が破棄されずにイベントを発火し続けている。
flatMap
とflatMapLatest
について
flatMap
// Projects each element of an observable sequence to an observable sequence and merges the resulting observable sequences into one observable sequence.
public func flatMap<O>(_ selector: @escaping (Self.E) throws -> O) -> RxSwift.Observable<O.E> where O : ObservableConvertibleType
上のマーブル図では赤のイベントでObservableが生成されて、次の緑のイベントでもObservableが生成されて、赤と緑のObservableがマージされたObsrvableのイベントが結果流れている。
flatMapLatest
// It is a combination of `map` + `switchLatest` operator
public func flatMapLatest<O>(_ selector: @escaping (Self.E) throws -> O) -> RxSwift.Observable<O.E> where O : ObservableConvertibleType
→ 簡単に言えば map
+ switchLatest
ならswitchLatest
は?
flatMap
と flatMapLatest
の違いは?
→ merge
かswitchLatest
の違い。
flatMap
は新しく生成されたObservableがmerge
されて、flatMapLatest
ではswitchLatest
される。
なので、flatMap
の場合はイベントで生成されたObservableがmerge
されるので、生成されたObservableがcompletionしない限り増えていくが、flatMapLatest
は新しくObservableが生成されると前のObservableの購読を中止するため増えない。
今回のTimerが増えていったのは、flatMap
を使っていたためTimerのObservableが次々にmergeされたからである。
改善
flatMap
の箇所をflatMapLatest
に変えて無事に動きました。
class ViewController: UIViewController {
@IBOutlet weak var timerLabel: UILabel!
@IBOutlet weak var timerSwitch: UISwitch!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
timerSwitch.rx.value
.flatMapLatest { $0 ? Observable<Int>.interval(0.1, scheduler: MainScheduler.instance) : .empty() }
.map { String($0) }
.bind(to: timerLabel.rx.text)
.disposed(by: disposeBag)
}
}
まとめ
- インクリメンタルサーチなどでUIの更新を抑えるために使われる
flatMapLatest
の他の使い所 - Observable.interval()
は、
Timer`と比べて、インスタンスの管理が楽で使いやすい -
flatMap
,flatMapLatest
,switchLatest
,merge
の理解