# combine-schedulers
**Repository Path**: BitBuilder/combine-schedulers
## Basic Information
- **Project Name**: combine-schedulers
- **Description**: https://github.com/pointfreeco/combine-schedulers
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-08-03
- **Last Updated**: 2022-08-05
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# ⏰ Combine Schedulers
[](https://github.com/pointfreeco/combine-schedulers/actions?query=workflow%3ACI)
[](https://swiftpackageindex.com/pointfreeco/combine-schedulers)
[](https://swiftpackageindex.com/pointfreeco/combine-schedulers)
A few schedulers that make working with Combine more testable and more versatile.
* [Motivation](#motivation)
* [Learn more](#learn-more)
* [`AnyScheduler`](#anyscheduler)
* [`TestScheduler`](#testscheduler)
* [`ImmediateScheduler`](#immediatescheduler)
* [Animated schedulers](#animated-schedulers)
* [`UnimplementedScheduler`](#unimplementedscheduler)
* [`UIScheduler`](#uischeduler)
* [`Publishers.Timer`](#publisherstimer)
* [Installation](#installation)
* [Documentation](#documentation)
* [Other libraries](#other-libraries)
## Motivation
The Combine framework provides the `Scheduler` protocol, which is a powerful abstraction for describing how and when units of work are executed. It unifies many disparate ways of executing work, such as `DispatchQueue`, `RunLoop` and `OperationQueue`.
However, the moment you use any of these schedulers in your reactive code you instantly make the publisher asynchronous and therefore much harder to test, forcing you to use expectations and waits for time to pass as your publisher executes.
This library provides new schedulers that allow you to turn any asynchronous publisher into a synchronous one for ease of testing and debugging.
## Learn More
This library was designed over the course of many episodes on [Point-Free](https://www.pointfree.co), a video series exploring functional programming and Swift hosted by [Brandon Williams](https://github.com/mbrandonw) and [Stephen Celis](https://github.com/stephencelis).
You can watch all of the episodes [here](https://www.pointfree.co/collections/combine/schedulers).
### `AnyScheduler`
The `AnyScheduler` provides a type-erasing wrapper for the `Scheduler` protocol, which can be useful for being generic over many types of schedulers without needing to actually introduce a generic to your code. The Combine framework ships with many type-erasing wrappers, such as `AnySubscriber`, `AnyPublisher` and `AnyCancellable`, yet for some reason does not ship with `AnyScheduler`.
This type is useful for times that you want to be able to customize the scheduler used in some code from the outside, but you don't want to introduce a generic to make it customizable. For example, suppose you have an `ObservableObject` view model that performs an API request when a method is called:
```swift
class EpisodeViewModel: ObservableObject {
@Published var episode: Episode?
let apiClient: ApiClient
init(apiClient: ApiClient) {
self.apiClient = apiClient
}
func reloadButtonTapped() {
self.apiClient.fetchEpisode()
.receive(on: DispatchQueue.main)
.assign(to: &self.$episode)
}
}
```
Notice that we are using `DispatchQueue.main` in the `reloadButtonTapped` method because the `fetchEpisode` endpoint most likely delivers its output on a background thread (as is the case with `URLSession`).
This code seems innocent enough, but the presence of `.receive(on: DispatchQueue.main)` makes this code harder to test since you have to use `XCTest` expectations to explicitly wait a small amount of time for the queue to execute. This can lead to flakiness in tests and make test suites take longer to execute than necessary.
One way to fix this testing problem is to use an ["immediate" scheduler](#immediatescheduler) instead of `DispatchQueue.main`, which will cause `fetchEpisode` to deliver its output as soon as possible with no thread hops. In order to allow for this we would need to inject a scheduler into our view model so that we can control it from the outside:
```swift
class EpisodeViewModel: ObservableObject {
@Published var episode: Episode?
let apiClient: ApiClient
let scheduler: S
init(apiClient: ApiClient, scheduler: S) {
self.apiClient = apiClient
self.scheduler = scheduler
}
func reloadButtonTapped() {
self.apiClient.fetchEpisode()
.receive(on: self.scheduler)
.assign(to: &self.$episode)
}
}
```
Now we can initialize this view model in production by using `DispatchQueue.main` and we can initialize it in tests using `DispatchQueue.immediate`. Sounds like a win!
However, introducing this generic to our view model is quite heavyweight as it is loudly announcing to the outside world that this type uses a scheduler, and worse it will end up infecting any code that touches this view model that also wants to be testable. For example, any view that uses this view model will need to introduce a generic if it wants to also be able to control the scheduler, which would be useful if we wanted to write [snapshot tests](https://github.com/pointfreeco/swift-snapshot-testing).
Instead of introducing a generic to allow for substituting in different schedulers we can use `AnyScheduler`. It allows us to be somewhat generic in the scheduler, but without actually introducing a generic.
Instead of holding a generic scheduler in our view model we can say that we only want a scheduler whose associated types match that of `DispatchQueue`:
```swift
class EpisodeViewModel: ObservableObject {
@Published var episode: Episode?
let apiClient: ApiClient
let scheduler: AnySchedulerOf
init(apiClient: ApiClient, scheduler: AnySchedulerOf) {
self.apiClient = apiClient
self.scheduler = scheduler
}
func reloadButtonTapped() {
self.apiClient.fetchEpisode()
.receive(on: self.scheduler)
.assign(to: &self.$episode)
}
}
```
Then, in production we can create a view model that uses a live `DispatchQueue`, but we just have to first erase its type:
```swift
let viewModel = EpisodeViewModel(
apiClient: ...,
scheduler: DispatchQueue.main.eraseToAnyScheduler()
)
```
For common schedulers, like `DispatchQueue`, `OperationQueue`, and `RunLoop`, there is even a static helper on `AnyScheduler` that further simplifys this:
```swift
let viewModel = EpisodeViewModel(
apiClient: ...,
scheduler: .main
)
```
Then in tests we can use an immediate scheduler:
```swift
let viewModel = EpisodeViewModel(
apiClient: ...,
scheduler: .immediate
)
```
So, in general, `AnyScheduler` is great for allowing one to control what scheduler is used in classes, functions, etc. without needing to introduce a generic, which can help simplify the code and reduce implementation details from leaking out.
### `TestScheduler`
A scheduler whose current time and execution can be controlled in a deterministic manner. This scheduler is useful for testing how the flow of time effects publishers that use asynchronous operators, such as `debounce`, `throttle`, `delay`, `timeout`, `receive(on:)`, `subscribe(on:)` and more.
For example, consider the following `race` operator that runs two futures in parallel, but only emits the first one that completes:
```swift
func race