Simple Timer with Rxswift

countdown timer by `RxSwift`

Better approach for existing answer.

let countDown = 15 // 15 seconds 
Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
.take(countDown+1)
.subscribe(onNext: { timePassed in
let count = self.countDown - timePassed
print(count)

}, onCompleted: {
print("count down complete")
})

Rx Swift simple timer not working

DisposeBag will dispose of your subscription once it goes out of scope. In this instance, it'll be right after the call to subscribe, and it explains why you don't see anything printed to the console.

Move the definition of dispose bag to the class creating the subscription and everything should work fine.

class MyViewController: UIViewController {
let bag:DisposeBag = DisposeBag()

dynamic func onButtonTapped() {
var sig:Observable<Int>!
sig = Observable<Int>.interval(1.0, scheduler: MainScheduler.instance)

sig.subscribe(onNext: { (sec) in
print("Sec: \(sec)")
}).addDisposableTo(bag)
}
}

On a side note, interval expects an interval in seconds, so it will only tick every seconds as oposed to milliseconds.

RxSwift - start and update count down timer

What you're trying to achieve kinda sounds like a state machine.

You could achieve it by splitting the timer actions into actual "Actions" and merging them based on the trigger (one being the manual "add two seconds", and thee other is automated as "reduce one second").

I haven't fully tested it, but this can be a good starting ground:

enum TimerAction {
case tick
case addTwoSeconds
}

let trigger = PublishRelay<Void>()

let timer = Observable<Int>
.interval(.seconds(1), scheduler: MainScheduler.instance)
.map { _ in TimerAction.tick }

let addSeconds = trigger.map { TimerAction.addTwoSeconds }

Observable
.merge(timer, addSeconds)
.scan(into: 15) { totalSeconds, action in
totalSeconds += action == .addTwoSeconds ? 2 : -1
}
.takeUntil(.inclusive) { $0 == 0 }
.subscribe()
.disposed(by: disposeBag)

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
trigger.accept(()) // increase the timer by two seconds after 5 seconds
}

How do I make a resettable RxSwift Timer?

Update

In the comments, I was asked to justify why I suggested making a test for "new code". Part of the answer was that you should never accept the first draft of your code. As any composition teacher would tell you, don't turn in your first draft, spend some time refining what you write (with peer review if you can get it.) So given that and the fact that my tests missed one of the specifications, I was going to replace my initial answer with this more refined version, but I think it is instructive to keep the original so it can be compared to the refined answer.

In the below, you will see that I have updated the tests to accommodate the new specification and refined the code.

The fact that there is a flatMap in the function implies that there are two abstractions here. So I broke that out into a separate function.

The fact that I have enums with two case implies that I could use a Bool instead and remove the switches.

class rx_sandboxTests: XCTestCase {

func testPause() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(20, ())])
let reset = scheduler.createColdObservable([.next(30, ())])
let result = scheduler.start {
isPaused(pause: pause.asObservable(), reset: reset.asObservable())
}
XCTAssertEqual(result.events, [.next(200, true), .next(210, false), .next(220, true)])
}

func testTimerStart() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ())])
let reset = scheduler.createColdObservable([Recorded<Event<Void>>]())

let result = scheduler.start {
timer(initial: 10, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(200, 10), .next(211, 9), .next(212, 8), .next(213, 7), .next(214, 6), .next(215, 5), .next(216, 4), .next(217, 3), .next(218, 2), .next(219, 1), .next(220, 0)])
}

func testPausedTimer() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(13, ()), .next(20, ())])
let reset = scheduler.createColdObservable([Recorded<Event<Void>>]())

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(200, 4), .next(211, 3), .next(212, 2), .next(221, 1), .next(222, 0)])
}

func testResetBeforeStarting() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(20, ())])
let reset = scheduler.createColdObservable([.next(10, ())])

let result = scheduler.start {
timer(initial: 3, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(200, 3), .next(221, 2), .next(222, 1), .next(223, 0)])
}

func testResetWhileRunning() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(20, ())])
let reset = scheduler.createColdObservable([.next(13, ())])

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(200, 4), .next(211, 3), .next(212, 2), .next(213, 4), .next(221, 3), .next(222, 2), .next(223, 1), .next(224, 0)])
}

func testResetWhilePaused() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(13, ()), .next(20, ())])
let reset = scheduler.createColdObservable([.next(15, ())])

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(200, 4), .next(211, 3), .next(212, 2), .next(215, 4), .next(221, 3), .next(222, 2), .next(223, 1), .next(224, 0)])
}

func testResetWhenEnded() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(20, ())])
let reset = scheduler.createColdObservable([.next(15, ())])

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(200, 4), .next(211, 3), .next(212, 2), .next(213, 1), .next(214, 0), .next(215, 4), .next(221, 3), .next(222, 2), .next(223, 1), .next(224, 0)])
}
}

func timer(initial: Int, pause: Observable<Void>, reset: Observable<Void>, scheduler: SchedulerType) -> Observable<Int> {
let tick = isPaused(pause: pause, reset: reset)
.flatMapLatest { $0 ? .empty() : Observable<Int>.interval(.seconds(1), scheduler: scheduler).take(initial) }

return ticker(initial: initial, tick: tick, reset: reset)
}

func isPaused(pause: Observable<Void>, reset: Observable<Void>) -> Observable<Bool> {
Observable.merge(pause.map { false }, reset.map { true })
.scan(true) { $1 || !$0 }
.startWith(true)
.distinctUntilChanged()
}

func ticker<T>(initial: Int, tick: Observable<T>, reset: Observable<Void>) -> Observable<Int> {
return Observable.merge(tick.map { _ in false }, reset.map { true })
.scan(initial) { $1 ? initial : $0 - 1 }
.startWith(initial)
.filter { 0 <= $0 }
.distinctUntilChanged()
}

Original Answer Follows:

I changed your pause from an Observable<Bool> to Observable<Void>. The Bool didn't make any sense because the reset can also cause a pause and that would conflict with the other observable.

Here's the complete code, including a test harness:

class rx_sandboxTests: XCTestCase {

func testTimerStart() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ())])
let reset = scheduler.createColdObservable([Recorded<Event<Void>>]())

let result = scheduler.start {
timer(initial: 10, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(211, 9), .next(212, 8), .next(213, 7), .next(214, 6), .next(215, 5), .next(216, 4), .next(217, 3), .next(218, 2), .next(219, 1), .next(220, 0)])
}

func testPause() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(13, ()), .next(20, ())])
let reset = scheduler.createColdObservable([Recorded<Event<Void>>]())

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(211, 3), .next(212, 2), .next(221, 1), .next(222, 0)])
}

func testResetBeforeStarting() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(20, ())])
let reset = scheduler.createColdObservable([.next(10, ())])

let result = scheduler.start {
timer(initial: 3, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(221, 2), .next(222, 1), .next(223, 0)])
}

func testResetWhileRunning() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(20, ())])
let reset = scheduler.createColdObservable([.next(13, ())])

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(211, 3), .next(212, 2), .next(221, 3), .next(222, 2), .next(223, 1), .next(224, 0)])
}

func testResetWhilePaused() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(13, ()), .next(20, ())])
let reset = scheduler.createColdObservable([.next(15, ())])

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(211, 3), .next(212, 2), .next(221, 3), .next(222, 2), .next(223, 1), .next(224, 0)])
}

func testResetWhenEnded() {
let scheduler = TestScheduler(initialClock: 0)
let pause = scheduler.createColdObservable([.next(10, ()), .next(20, ())])
let reset = scheduler.createColdObservable([.next(15, ())])

let result = scheduler.start {
timer(initial: 4, pause: pause.asObservable(), reset: reset.asObservable(), scheduler: scheduler)
}

XCTAssertEqual(result.events, [.next(211, 3), .next(212, 2), .next(213, 1), .next(214, 0), .next(221, 3), .next(222, 2), .next(223, 1), .next(224, 0)])
}
}

func timer(initial: Int, pause: Observable<Void>, reset: Observable<Void>, scheduler: SchedulerType) -> Observable<Int> {
enum Action { case pause, reset, tick }
let intent = Observable.merge(
pause.map { Action.pause },
reset.map { Action.reset }
)

let isPaused = intent
.scan(true) { isPaused, action in
switch action {
case .pause:
return !isPaused
case .reset:
return true
case .tick:
fatalError()
}
}
.startWith(true)

let tick = isPaused
.flatMapLatest { $0 ? .empty() : Observable<Int>.interval(.seconds(1), scheduler: scheduler) }

return Observable.merge(tick.map { _ in Action.tick }, reset.map { Action.reset })
.scan(initial) { (current, action) -> Int in
switch action {
case .pause:
fatalError()
case .reset:
return initial
case .tick:
return current == -1 ? -1 : current - 1
}

}
.filter { 0 <= $0 && $0 < initial }
}

It's good to know how to test Rx code.

RxSwift Countdown time up to 0.1 seconds

Use .milliseconds(100) instead of .seconds(1)

Combine RxSwift timer and interval observables into a single observable with completion

Is that like this?

CountdownTimer.swift

var timer = CountdownTimer(5)
var count = 0

func setupCountdownTimer() {
timer.observable >- subscribeNext { n in
println(n) // "5", "4", ..., "0"
self.count = n
}
timer.observable >- subscribeCompleted {
println(self.count)
}
}

@IBAction func stop(sender: UIButton) {
timer.sendCompleted()
}

Correct way to restart observable interval in RxSwift

What you're looking for is merge. You have two Observables, one of which is an interval and the other which represents preference changes. You want to merge those into one Observable with the elements from both, immediately as they come.

That would look like this:

// this should really come from somewhere else in your app
let preferencesChanged = PublishSubject<Void>()

// the `map` is so that the element type goes from `Int` to `Void`
// since the `merge` requires that the element types match
let timer = Observable<Int>.timer(0, period: 3, scheduler: MainScheduler.instance).map { _ in () }

Observable.of(timer, preferencesChanged)
.merge()
.flatMapLatest(makeRequest)
.subscribeNext(setSummary)
.addDisposableTo(disposeBag)

Also notice how I'm using timer instead of interval, since it allows us to specify when to fire for the first time, as well as the period for subsequent firings. That way, you don't need a startWith. However, both ways work. It's a matter of preference.

One more thing to note. This is outside of the scope of your question (and maybe you kept it simple for the sake of the question) but instead of subscribeNext(setSummary), you should consider keeping the result as an Observable and instead bindTo or drive the UI or DB (or whatever "summary" is).

How to test RxSwift Observable.interval progress

Yea, check out this simple test that has the behavior you're looking for: https://github.com/ReactiveX/RxSwift/blob/b3f4bf1/Tests/RxSwiftTests/Tests/Observable+TimeTest.swift#L496

To swap TestScheduler for MainScheduler, I would suggest you inject it as a dependency.

Also, to check on the value of _exerciseRemainingTime, you would need to remove private. I wouldn't suggest testing the internals of your class, however. Removing private is a sign that you are. Instead, if you were to inject an object whose responsibility is to do progressToNextExercise, then you could test that it received a call to progress to the next exercise. You would just pass in a test version of that object during tests, like you do with TestScheduler for the scheduler. That would remove the need to make _exerciseRemainingTime public to test it, or even know about it.

However, ignoring the visibility of _exerciseRemainingTime for the sake of this question's main purpose, here's what I mean about the scheduler:

WorkoutViewModel.swift

class WorkoutViewModel {
private var _oneSecondTimer: Observable<Int> {
return Observable<Int>.interval(1, scheduler: scheduler)
}

// not `private` anymore. also, a computed property
var _exerciseRemainingTime: Observable<Int> {
return self._oneSecondTimer.map { i in
20 - i
}
}

// injected via `init`
private let scheduler: SchedulerType

init(scheduler: SchedulerType) {
self.scheduler = scheduler
}
}

WorkoutViewModelTest.swift

func testExerciseRemainingTime() {
let scheduler = TestScheduler(initialClock: 0)

let res = scheduler.start(0, subscribed: 0, disposed: 23) {
WorkoutViewModel(scheduler: scheduler)._exerciseRemainingTime
}

let correct = [
next(1, 20), // output .Next(20) at 1 second mark
next(2, 19), // output .Next(19) at 2 second mark
next(3, 18),
next(4, 17),
next(5, 16),
next(6, 15),
next(7, 14),
next(8, 13),
next(9, 12),
next(10, 11),
next(11, 10),
next(12, 9),
next(13, 8),
next(14, 7),
next(15, 6),
next(16, 5),
next(17, 4),
next(18, 3),
next(19, 2),
next(20, 1),
next(21, 0),
next(22, -1),
]

XCTAssertEqual(res.events, correct)
}

A couple notes to consider:

To let the test scheduler subscribe and dispose, I removed the subscribeNext from the view model. I think this improves it anyway, as you should be subscribing with the view controller and only using the view model to provide you with Observable. This obviates the need for the view model to have a dispose bag and manage the lifecycle of Disposables.

You should really consider exposing something less "internal" than _exerciseRemainingTime. Maybe something like currentExercise: Observable<ExerciseEnum>, which is internally based on _exerciseRemainingTime. That way, your view controller can subscribe and do the simple view controller-related job of segueing into the next exercise.

Also, to simplify the test, you can inject the 20 variable into the view model so that in tests you can supply something smaller like 3 and then correct will only need to be a few elements long.

Updating periodically with RxSwift

This is very similar to this question/answer.

You should use timer and then flatMapLatest:

Observable<Int>.timer(0, period: 20, scheduler: MainScheduler.instance)
.flatMapLatest { _ in
provider.request(.Issue)
}
.mapArray(Issue.self, keyPath: "issues")
// ...


Related Topics



Leave a reply



Submit