Files
swift-composable-architectu…/Sources/ComposableArchitecture/Effects/Timer.swift
Stephen Celis f0098d8232 Add cancel overloads that take types (#1078)
* Add cancel overloads that take types

* wip
2022-05-03 21:00:24 -04:00

144 lines
5.5 KiB
Swift

import Combine
import CombineSchedulers
extension Effect where Failure == Never {
/// Returns an effect that repeatedly emits the current time of the given scheduler on the given
/// interval.
///
/// While it is possible to use Foundation's `Timer.publish(every:tolerance:on:in:options:)` API
/// to create a timer in the Composable Architecture, it is not advisable. This API only allows
/// creating a timer on a run loop, which means when writing tests you will need to explicitly
/// wait for time to pass in order to see how the effect evolves in your feature.
///
/// In the Composable Architecture we test time-based effects like this by using the
/// `TestScheduler`, which allows us to explicitly and immediately advance time forward so that
/// we can see how effects emit. However, because `Timer.publish` takes a concrete `RunLoop` as
/// its scheduler, we can't substitute in a `TestScheduler` during tests`.
///
/// That is why we provide `Effect.timer`. It allows you to create a timer that works with any
/// scheduler, not just a run loop, which means you can use a `DispatchQueue` or `RunLoop` when
/// running your live app, but use a `TestScheduler` in tests.
///
/// To start and stop a timer in your feature you can create the timer effect from an action
/// and then use the ``Effect/cancel(id:)-iun1`` effect to stop the timer:
///
/// ```swift
/// struct AppState {
/// var count = 0
/// }
///
/// enum AppAction {
/// case startButtonTapped, stopButtonTapped, timerTicked
/// }
///
/// struct AppEnvironment {
/// var mainQueue: AnySchedulerOf<DispatchQueue>
/// }
///
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in
/// struct TimerId: Hashable {}
///
/// switch action {
/// case .startButtonTapped:
/// return Effect.timer(id: TimerId(), every: 1, on: env.mainQueue)
/// .map { _ in .timerTicked }
///
/// case .stopButtonTapped:
/// return .cancel(id: TimerId())
///
/// case let .timerTicked:
/// state.count += 1
/// return .none
/// }
/// ```
///
/// Then to test the timer in this feature you can use a test scheduler to advance time:
///
/// ```swift
/// func testTimer() {
/// let scheduler = DispatchQueue.test
///
/// let store = TestStore(
/// initialState: .init(),
/// reducer: appReducer,
/// environment: .init(
/// mainQueue: scheduler.eraseToAnyScheduler()
/// )
/// )
///
/// store.send(.startButtonTapped)
///
/// scheduler.advance(by: .seconds(1))
/// store.receive(.timerTicked) { $0.count = 1 }
///
/// scheduler.advance(by: .seconds(5))
/// store.receive(.timerTicked) { $0.count = 2 }
/// store.receive(.timerTicked) { $0.count = 3 }
/// store.receive(.timerTicked) { $0.count = 4 }
/// store.receive(.timerTicked) { $0.count = 5 }
/// store.receive(.timerTicked) { $0.count = 6 }
///
/// store.send(.stopButtonTapped)
/// }
/// ```
///
/// - Note: This effect is only meant to be used with features built in the Composable
/// Architecture, and returned from a reducer. If you want a testable alternative to
/// Foundation's `Timer.publish` you can use the publisher `Publishers.Timer` that is included
/// in this library via the
/// [`CombineSchedulers`](https://github.com/pointfreeco/combine-schedulers) module.
///
/// - Parameters:
/// - id: The effect's identifier.
/// - interval: The time interval on which to publish events. For example, a value of `0.5`
/// publishes an event approximately every half-second.
/// - scheduler: The scheduler on which the timer runs.
/// - tolerance: The allowed timing variance when emitting events. Defaults to `nil`, which
/// allows any variance.
/// - options: Scheduler options passed to the timer. Defaults to `nil`.
public static func timer<S>(
id: AnyHashable,
every interval: S.SchedulerTimeType.Stride,
tolerance: S.SchedulerTimeType.Stride? = nil,
on scheduler: S,
options: S.SchedulerOptions? = nil
) -> Effect where S: Scheduler, S.SchedulerTimeType == Output {
Publishers.Timer(every: interval, tolerance: tolerance, scheduler: scheduler, options: options)
.autoconnect()
.setFailureType(to: Failure.self)
.eraseToEffect()
.cancellable(id: id, cancelInFlight: true)
}
/// Returns an effect that repeatedly emits the current time of the given scheduler on the given
/// interval.
///
/// A convenience for calling ``Effect/timer(id:every:tolerance:on:options:)-4exe6`` with a
/// static type as the effect's unique identifier.
///
/// - Parameters:
/// - id: A unique type identifying the effect.
/// - interval: The time interval on which to publish events. For example, a value of `0.5`
/// publishes an event approximately every half-second.
/// - scheduler: The scheduler on which the timer runs.
/// - tolerance: The allowed timing variance when emitting events. Defaults to `nil`, which
/// allows any variance.
/// - options: Scheduler options passed to the timer. Defaults to `nil`.
public static func timer<S>(
id: Any.Type,
every interval: S.SchedulerTimeType.Stride,
tolerance: S.SchedulerTimeType.Stride? = nil,
on scheduler: S,
options: S.SchedulerOptions? = nil
) -> Effect where S: Scheduler, S.SchedulerTimeType == Output {
self.timer(
id: ObjectIdentifier(id),
every: interval,
tolerance: tolerance,
on: scheduler,
options: options
)
}
}