CombineSchedulers Documentation

Structure Any​Scheduler

public struct AnyScheduler<SchedulerTimeType, SchedulerOptions>: Scheduler, @unchecked Sendable
  where
    SchedulerTimeType: Strideable,
    SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible

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.

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 a view model ObservableObject that performs an API request when a method is called:

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

class EpisodeViewModel<S: Scheduler>: 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.

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:

class EpisodeViewModel: ObservableObject {
  @Published var episode: Episode?

  let apiClient: ApiClient
  let scheduler: AnySchedulerOf<DispatchQueue>

  init(apiClient: ApiClient, scheduler: AnySchedulerOf<DispatchQueue>) {
    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:

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 simplifies this:

let viewModel = EpisodeViewModel(
  apiClient: ...,
  scheduler: .main
)

And in tests we can use an immediate scheduler:

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.

%21 AnyScheduler AnyScheduler @unchecked Sendable @unchecked Sendable AnyScheduler->@unchecked Sendable Scheduler Scheduler AnyScheduler->Scheduler

Conforms To

@unchecked Sendable
Scheduler

Initializers

init(minimum​Tolerance:​now:​schedule​Immediately:​delayed:​interval:​)

public init(
      minimumTolerance: @escaping () -> SchedulerTimeType.Stride,
      now: @escaping () -> SchedulerTimeType,
      scheduleImmediately: @escaping (SchedulerOptions?, @escaping () -> Void) -> Void,
      delayed: @escaping (
        SchedulerTimeType, SchedulerTimeType.Stride, SchedulerOptions?, @escaping () -> Void
      ) -> Void,
      interval: @escaping (
        SchedulerTimeType, SchedulerTimeType.Stride, SchedulerTimeType.Stride, SchedulerOptions?,
        @escaping () -> Void
      ) -> Cancellable
    )  

Creates a type-erasing scheduler to wrap the provided endpoints.

Parameters

minimum​Tolerance @escaping () -> Scheduler​Time​Type.​Stride

A closure that returns the scheduler's minimum tolerance.

now @escaping () -> Scheduler​Time​Type

A closure that returns the scheduler's current time.

schedule​Immediately @escaping (Scheduler​Options?, @escaping () -> Void) -> Void

A closure that schedules a unit of work to be run as soon as possible.

delayed @escaping ( Scheduler​Time​Type, Scheduler​Time​Type.​Stride, Scheduler​Options?, @escaping () -> Void ) -> Void

A closure that schedules a unit of work to be run after a delay.

interval @escaping ( Scheduler​Time​Type, Scheduler​Time​Type.​Stride, Scheduler​Time​Type.​Stride, Scheduler​Options?, @escaping () -> Void ) -> Cancellable

A closure that schedules a unit of work to be performed on a repeating interval.

init(_:​)

public init<S>(
      _ scheduler: S
    )
    where
      S: Scheduler, S.SchedulerTimeType == SchedulerTimeType, S.SchedulerOptions == SchedulerOptions

Creates a type-erasing scheduler to wrap the provided scheduler.

Parameters

scheduler S

A scheduler to wrap with a type-eraser.

Properties

minimum​Tolerance

public var minimumTolerance: SchedulerTimeType.Stride  

The minimum tolerance allowed by the scheduler.

now

public var now: SchedulerTimeType  

This scheduler’s definition of the current moment in time.

Methods

schedule(after:​tolerance:​options:​_:​)

public func schedule(
      after date: SchedulerTimeType,
      tolerance: SchedulerTimeType.Stride,
      options: SchedulerOptions?,
      _ action: @escaping () -> Void
    )  

Performs the action at some time after the specified date.

schedule(after:​interval:​tolerance:​options:​_:​)

public func schedule(
      after date: SchedulerTimeType,
      interval: SchedulerTimeType.Stride,
      tolerance: SchedulerTimeType.Stride,
      options: SchedulerOptions?,
      _ action: @escaping () -> Void
    ) -> Cancellable  

Performs the action at some time after the specified date, at the specified frequency, taking into account tolerance if possible.

schedule(options:​_:​)

public func schedule(
      options: SchedulerOptions?,
      _ action: @escaping () -> Void
    )  

Performs the action at the next possible opportunity.