RxSwift - start and update count down timer

Viewed 592

I'm trying to build a timer that starts at 15 seconds and count down until 0.

The thing is that I'll want to update that timer by 2 seconds more based on an event.

This is what I've tried to do so far:

struct ViewModel {
    struct Input {
        let add_time: Observable<Void>
    }
    struct Output {
        let current_time: Observable<String>
        let timer_over: Observable<Void>
    }

    private let current_time = BehaviorRelay(value: 15)
    private let timer_over = PublishSubject<Void>()
    
    func transform(input: Input) -> Output {
        let current_time = self.current_time
            .flatMapLatest { time in
                Observable<Int>
                    .timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
                    .take(self.current_time.value + 1)
                    .map { "\(self.current_time.value - $0)" }
                    .do(onCompleted: { self.timer_over.onNext(()) })
            }
        return Output(
            current_time: current_time,
            timer_over: timer_over
        )
    }
}

let disposeBag = DisposeBag()

let add_time_subject = PublishSubject<Void>()
let input = ViewModel.Input(
    add_time: add_time_subject.asObservable()
)
let output = ViewModel().transform(input: input)
output.current_time
    .subscribe(onNext: { (time) in
        print(time)
    })
    .disposed(by: disposeBag)
output.timer_over
    .subscribe(onNext: {
        print("timer_over")
    })
    .disposed(by: disposeBag)

The thing is when I run add_time_subject.onNext(()) I'd want to update the timer by 2 seconds more only if the timer hasn't reached 0 seconds.

How should I do that?

1 Answers

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
}
Related