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 Observable
s, 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 Disposable
s.
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
Get the First Day of Week Without Weekcalendarunit
Capturing a Struct Reference in a Closure Doesn't Allow Mutations to Occur
Hittest Prints Ar Entity Name Even When I am Not Tapping on It
Calculate the Number of Dimensions of a Multi-Dimensional Array in Swift
How to Reduce the Opacity of the Shadows in Realitykit
Guard Let Error: Initializer for Conditional Binding Must Have Optional Type Not 'String'
How to Place Uisearchcontroller to the Navigationtitle and How to Enable and Disable It by Button
Loading Views into Nscontainerview with Swift
Integrating Congnito User Pools with Amazon Cognito Identity
Coerced to Any' But Property Is of Type Uicolor
How to Use Tabs to Evenly Space Out Description Strings in Swift
How to Segue Values When My Viewcontroller Is Embedded in an Uinavigationcontroller
Deep Copy of Cmimagebuffer or Cvimagebuffer
Why Does Somestruct() Is Anyobject Return True
Concatenate Literal with Optional String