Как сделать сбрасываемый таймер RxSwift? - PullRequest
2 голосов
/ 25 апреля 2020

Я пытаюсь создать приложение таймера обратного отсчета. Естественно, мне нужна последовательность Observable<Int>, которая дает мне элемент каждую секунду. Суть в том, что мне нужна эта последовательность, чтобы реагировать на изменения двух других последовательностей:

  • Приостановлено: Observable<Bool>. Это моделирует пользователя, нажимающего кнопку паузы / возобновления
  • Сброс: Observable<Void>. Это моделирует пользователя, нажимающего кнопку сброса.

Кнопка сброса вернет таймер к его начальному значению (resetState), , а также приостановит таймер .

Пользователь может нажать кнопку сброса в любое время:

  1. сбросить, когда таймер не был запущен
  2. сбросить, когда таймер работает, не приостановлен, не завершен
  3. Сброс, когда таймер приостановлен, не завершен
  4. Сброс, когда таймер закончился

Объединение ответов на этот вопрос и этот вопрос , я смог придумать класс Timer, подобный этому:

class Timer {
    var paused = true
    {
        didSet {
            rxPaused.accept(paused)
        }
    }
    var ended = false

    let rxPaused = BehaviorRelay(value: true)
    let disposeBag = DisposeBag()

    // timerEvents is the observable that client code should subscribe to
    var timerEvents: Observable<Int>!

    var currentState: Int
    let resetState: Int

    init(resetState: Int) {
        self.currentState = resetState
        self.resetState = resetState
        reset()
    }

    func start() {
        if !ended {
            paused = false
        }
    }

    func pause() {
        paused = true
    }

    func reset() {
        ended = false
        currentState = resetState
        pause()
        timerEvents = rxPaused.asObservable()
            .flatMapLatest {  isRunning in
                isRunning ? .empty() : Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
            }
            .enumerated().flatMap { (index, int) in Observable.just(index) }
        .map { [weak self] x in (self?.resetState ?? x) - x }
            .take(self.resetState)
        timerEvents.subscribe(onNext: { [weak self]
            timerEvent in
            self?.currentState -= 1
            }, onCompleted: {
                [weak self] in
                self?.ended = true
        }).disposed(by: disposeBag)
    }
}

Использование:

let timer = Timer(resetState: 20)
timer.timerEvents.subscribe(
    onNext: { [unowned self] (timerEvent) in
        print(timerEvent.state)
        self.updateTimerLabelText()
}).disposed(by: disposeBag)

Это работает только так, как ожидалось в ситуации 4 упомянуто выше.

Если вы попытаетесь сбросить этот таймер до его окончания, все становится смешно. Например, в ситуации 1 (сброс до четного запуска) timerEvents создает два одинаковых элемента каждую секунду. Это не только неверно, но также приводит к тому, что currentState уменьшается вдвое быстрее. Я подозреваю, что это потому, что timerEvents присваивается во второй раз, до того, как будет завершено его предыдущее значение, но я не думаю, что есть способ просто "завершить" незавершенную наблюдаемую, не так ли?

И я даже не могу выразить словами, что происходит в ситуациях 2 и 3.

Как я могу заставить этот таймер сброса работать?

1 Ответ

1 голос
/ 26 апреля 2020

Обновление

В комментариях меня попросили объяснить, почему я предложил сделать тест для "нового кода". Частично ответ состоял в том, что вы никогда не должны принимать первый черновик вашего кода. Как сказал бы любой учитель по композиции, не сдавайте свой первый набросок, потратьте некоторое время на уточнение того, что вы пишете (с подробным обзором, если вы можете его получить). Учитывая это и тот факт, что мои тесты не соответствовали одной из спецификаций, Я собирался заменить свой первоначальный ответ более уточненной версией, но я думаю, что полезно сохранить оригинал, чтобы его можно было сравнить с уточненным ответом.

Ниже вы увидите, что я имею обновил тесты для соответствия новой спецификации и уточнил код.

Тот факт, что в функции есть flatMap, подразумевает, что здесь есть две абстракции. Поэтому я разбил это на отдельную функцию.

Тот факт, что у меня есть перечисления с двумя регистрами, подразумевает, что я мог бы вместо этого использовать Bool и убрать переключатели.

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()
}

Оригинальный ответ следует :

Я изменил вашу паузу с Observable<Bool> на Observable<Void>. Bool не имеет никакого смысла, потому что сброс может также вызвать паузу, и это может конфликтовать с другими наблюдаемыми.

Вот полный код, включая тестовый набор:

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

Хорошо знать, как проверить код Rx.

...