{"id":51542766,"url":"https://github.com/d-date/combine-schedulers-android","last_synced_at":"2026-07-09T15:01:43.960Z","repository":{"id":366724314,"uuid":"1232538679","full_name":"d-date/combine-schedulers-android","owner":"d-date","description":"Private fork of pointfreeco/combine-schedulers with explicit OpenCombine dep on Android/Linux","archived":false,"fork":false,"pushed_at":"2026-05-08T03:12:19.000Z","size":180,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main-android","last_synced_at":"2026-06-23T04:29:01.279Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/d-date.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-08T03:07:16.000Z","updated_at":"2026-05-08T05:12:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/d-date/combine-schedulers-android","commit_stats":null,"previous_names":["d-date/combine-schedulers-android"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/d-date/combine-schedulers-android","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fcombine-schedulers-android","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fcombine-schedulers-android/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fcombine-schedulers-android/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fcombine-schedulers-android/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/d-date","download_url":"https://codeload.github.com/d-date/combine-schedulers-android/tar.gz/refs/heads/main-android","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/d-date%2Fcombine-schedulers-android/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35303292,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-09T15:01:42.259Z","updated_at":"2026-07-09T15:01:43.945Z","avatar_url":"https://github.com/d-date.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ⏰ Combine Schedulers\n\n[![CI](https://github.com/pointfreeco/combine-schedulers/workflows/CI/badge.svg)](https://github.com/pointfreeco/combine-schedulers/actions?query=workflow%3ACI)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fcombine-schedulers%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/combine-schedulers)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fcombine-schedulers%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pointfreeco/combine-schedulers)\n\nA few schedulers that make working with Combine more testable and more versatile.\n\n* [Motivation](#motivation)\n* [Learn more](#learn-more)\n  * [`AnyScheduler`](#anyscheduler)\n  * [`TestScheduler`](#testscheduler)\n  * [`ImmediateScheduler`](#immediatescheduler)\n  * [Animated schedulers](#animated-schedulers)\n  * [`UnimplementedScheduler`](#unimplementedscheduler)\n  * [`UIScheduler`](#uischeduler)\n  * [Concurrency APIs](#concurrency-apis)\n  * [`Publishers.Timer`](#publisherstimer)\n* [Installation](#installation)\n* [Documentation](#documentation)\n* [Other libraries](#other-libraries)\n\n## Motivation\n\nThe 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`.\n\nHowever, 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.\n\nThis library provides new schedulers that allow you to turn any asynchronous publisher into a synchronous one for ease of testing and debugging.\n\n## Learn More\n\nThis 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).\n\nYou can watch all of the episodes [here](https://www.pointfree.co/collections/combine/schedulers).\n\n\u003ca href=\"https://www.pointfree.co/collections/combine/schedulers\"\u003e\n  \u003cimg alt=\"video poster image\" src=\"https://d3rccdn33rt8ze.cloudfront.net/episodes/0106.jpeg\" width=\"480\"\u003e\n\u003c/a\u003e\n\n### `AnyScheduler`\n\nThe `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`.\n\nThis 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:\n\n```swift\nclass EpisodeViewModel: ObservableObject {\n  @Published var episode: Episode?\n\n  let apiClient: ApiClient\n\n  init(apiClient: ApiClient) {\n    self.apiClient = apiClient\n  }\n\n  func reloadButtonTapped() {\n    self.apiClient.fetchEpisode()\n      .receive(on: DispatchQueue.main)\n      .assign(to: \u0026self.$episode)\n  }\n}\n```\n\nNotice 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`).\n\nThis 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.\n\nOne 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:\n\n```swift\nclass EpisodeViewModel\u003cS: Scheduler\u003e: ObservableObject {\n  @Published var episode: Episode?\n\n  let apiClient: ApiClient\n  let scheduler: S\n\n  init(apiClient: ApiClient, scheduler: S) {\n    self.apiClient = apiClient\n    self.scheduler = scheduler\n  }\n\n  func reloadButtonTapped() {\n    self.apiClient.fetchEpisode()\n      .receive(on: self.scheduler)\n      .assign(to: \u0026self.$episode)\n  }\n}\n```\n\nNow 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!\n\nHowever, 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).\n\nInstead 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.\n\nInstead 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`:\n\n```swift\nclass EpisodeViewModel: ObservableObject {\n  @Published var episode: Episode?\n\n  let apiClient: ApiClient\n  let scheduler: AnySchedulerOf\u003cDispatchQueue\u003e\n\n  init(apiClient: ApiClient, scheduler: AnySchedulerOf\u003cDispatchQueue\u003e) {\n    self.apiClient = apiClient\n    self.scheduler = scheduler\n  }\n\n  func reloadButtonTapped() {\n    self.apiClient.fetchEpisode()\n      .receive(on: self.scheduler)\n      .assign(to: \u0026self.$episode)\n  }\n}\n```\n\nThen, in production we can create a view model that uses a live `DispatchQueue`, but we just have to first erase its type:\n\n```swift\nlet viewModel = EpisodeViewModel(\n  apiClient: ...,\n  scheduler: DispatchQueue.main.eraseToAnyScheduler()\n)\n```\n\nFor common schedulers, like `DispatchQueue`, `OperationQueue`, and `RunLoop`, there is even a static helper on `AnyScheduler` that further simplifys this:\n\n```swift\nlet viewModel = EpisodeViewModel(\n  apiClient: ...,\n  scheduler: .main\n)\n```\n\nThen in tests we can use an immediate scheduler:\n\n```swift\nlet viewModel = EpisodeViewModel(\n  apiClient: ...,\n  scheduler: .immediate\n)\n```\n\nSo, 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.\n\n### `TestScheduler`\n\nA 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.\n\nFor example, consider the following `race` operator that runs two futures in parallel, but only emits the first one that completes:\n\n```swift\nfunc race\u003cOutput, Failure: Error\u003e(\n  _ first: Future\u003cOutput, Failure\u003e,\n  _ second: Future\u003cOutput, Failure\u003e\n) -\u003e AnyPublisher\u003cOutput, Failure\u003e {\n  first\n    .merge(with: second)\n    .prefix(1)\n    .eraseToAnyPublisher()\n}\n```\n\nAlthough this publisher is quite simple we may still want to write some tests for it.\n\nTo do this we can create a test scheduler and create two futures, one that emits after a second and one that emits after two seconds:\n\n```swift\nlet scheduler = DispatchQueue.test\n\nlet first = Future\u003cInt, Never\u003e { callback in\n  scheduler.schedule(after: scheduler.now.advanced(by: 1)) { callback(.success(1)) }\n}\nlet second = Future\u003cInt, Never\u003e { callback in\n  scheduler.schedule(after: scheduler.now.advanced(by: 2)) { callback(.success(2)) }\n}\n```\n\nAnd then we can race these futures and collect their emissions into an array:\n\n```swift\nvar output: [Int] = []\nlet cancellable = race(first, second).sink { output.append($0) }\n```\n\nAnd then we can deterministically move time forward in the scheduler to see how the publisher emits. We can start by moving time forward by one second:\n\n```swift\nscheduler.advance(by: 1)\nXCTAssertEqual(output, [1])\n```\n\nThis proves that we get the first emission from the publisher since one second of time has passed. If we further advance by one more second we can prove that we do not get anymore emissions:\n\n```swift\nscheduler.advance(by: 1)\nXCTAssertEqual(output, [1])\n```\n\nThis is a very simple example of how to control the flow of time with the test scheduler, but this technique can be used to test any publisher that involves Combine's asynchronous operations.\n\n### `ImmediateScheduler`\n\nThe Combine framework comes with an `ImmediateScheduler` type of its own, but it defines all new types for the associated types of `SchedulerTimeType` and `SchedulerOptions`. This means you cannot easily swap between a live `DispatchQueue` and an \"immediate\" `DispatchQueue` that executes work synchronously. The only way to do that would be to introduce generics to any code making use of that scheduler, which can become unwieldy.\n\nSo, instead, this library's `ImmediateScheduler` uses the same associated types as an existing scheduler, which means you can use `DispatchQueue.immediate` to have a scheduler that looks like a dispatch queue but executes its work immediately. Similarly you can construct `RunLoop.immediate` and `OperationQueue.immediate`.\n\nThis scheduler is useful for writing tests against publishers that use asynchrony operators, such as `receive(on:)`, `subscribe(on:)` and others, because it forces the publisher to emit immediately rather than needing to wait for thread hops or delays using `XCTestExpectation`.\n\nThis scheduler is different from `TestScheduler` in that you cannot explicitly control how time flows through your publisher, but rather you are instantly collapsing time into a single point.\n\nAs a basic example, suppose you have a view model that loads some data after waiting for 10 seconds from when a button is tapped:\n\n```swift\nclass HomeViewModel: ObservableObject {\n  @Published var episodes: [Episode]?\n\n  let apiClient: ApiClient\n\n  init(apiClient: ApiClient) {\n    self.apiClient = apiClient\n  }\n\n  func reloadButtonTapped() {\n    Just(())\n      .delay(for: .seconds(10), scheduler: DispatchQueue.main)\n      .flatMap { apiClient.fetchEpisodes() }\n      .assign(to: \u0026self.$episodes)\n  }\n}\n```\n\nIn order to test this code you would literally need to wait 10 seconds for the publisher to emit:\n\n```swift\nfunc testViewModel() {\n  let viewModel = HomeViewModel(apiClient: .mock)\n\n  viewModel.reloadButtonTapped()\n\n  _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 10)\n\n  XCTAssert(viewModel.episodes, [Episode(id: 42)])\n}\n```\n\nAlternatively, we can explicitly pass a scheduler into the view model initializer so that it can be controller from the outside:\n\n```swift\nclass HomeViewModel: ObservableObject {\n  @Published var episodes: [Episode]?\n\n  let apiClient: ApiClient\n  let scheduler: AnySchedulerOf\u003cDispatchQueue\u003e\n\n  init(apiClient: ApiClient, scheduler: AnySchedulerOf\u003cDispatchQueue\u003e) {\n    self.apiClient = apiClient\n    self.scheduler = scheduler\n  }\n\n  func reloadButtonTapped() {\n    Just(())\n      .delay(for: .seconds(10), scheduler: self.scheduler)\n      .flatMap { self.apiClient.fetchEpisodes() }\n      .assign(to: \u0026self.$episodes)\n  }\n}\n```\n\nAnd then in tests use an immediate scheduler:\n\n```swift\nfunc testViewModel() {\n  let viewModel = HomeViewModel(\n    apiClient: .mock,\n    scheduler: .immediate\n  )\n\n  viewModel.reloadButtonTapped()\n\n  // No more waiting...\n\n  XCTAssert(viewModel.episodes, [Episode(id: 42)])\n}\n```\n\n### Animated schedulers\n\nCombineSchedulers comes with helpers that aid in asynchronous animations in both SwiftUI and UIKit.\n\nIf a SwiftUI state mutation should be animated, you can invoke the `animation` and `transaction` methods to transform an existing scheduler into one that schedules its actions with an animation or in a transaction. These APIs mirror SwiftUI's `withAnimation` and `withTransaction` functions, which are invoked by the animated scheduler.\n\nFor example, to animate an API response in your view model, you can specify that the scheduler that receives this state should be animated:\n\n```swift\nself.apiClient.fetchEpisode()\n  .receive(on: self.scheduler.animation())\n  .assign(to: \u0026self.$episode)\n```\n\nIf you are powering a UIKit feature with Combine, you can use the `.animate` method, which mirrors `UIView.animate`:\n\n```swift\nself.apiClient.fetchEpisode()\n  .receive(on: self.scheduler.animate(withDuration: 0.3))\n  .assign(to: \u0026self.$episode)\n```\n\n### `UnimplementedScheduler`\n\nA scheduler that causes a test to fail if it is used.\n\nThis scheduler can provide an additional layer of certainty that a tested code path does not require the use of a scheduler.\n\nAs a view model becomes more complex, only some of its logic may require a scheduler. When writing unit tests for any logic that does _not_ require a scheduler, one should provide an unimplemented scheduler, instead. This documents, directly in the test, that the feature does not use a scheduler. If it did, or ever does in the future, the test will fail.\n\nFor example, the following view model has a couple responsibilities:\n\n```swift\nclass EpisodeViewModel: ObservableObject {\n  @Published var episode: Episode?\n\n  let apiClient: ApiClient\n  let mainQueue: AnySchedulerOf\u003cDispatchQueue\u003e\n\n  init(apiClient: ApiClient, mainQueue: AnySchedulerOf\u003cDispatchQueue\u003e) {\n    self.apiClient = apiClient\n    self.mainQueue = mainQueue\n  }\n\n  func reloadButtonTapped() {\n    self.apiClient.fetchEpisode()\n      .receive(on: self.mainQueue)\n      .assign(to: \u0026self.$episode)\n  }\n\n  func favoriteButtonTapped() {\n    self.episode?.isFavorite.toggle()\n  }\n}\n```\n\n  * It lets the user tap a button to refresh some episode data\n  * It lets the user toggle if the episode is one of their favorites\n\nThe API client delivers the episode on a background queue, so the view model must receive it on its main queue before mutating its state.\n\nTapping the favorite button, however, involves no scheduling. This means that a test can be written with an unimplemented scheduler:\n\n```swift\nfunc testFavoriteButton() {\n  let viewModel = EpisodeViewModel(\n    apiClient: .mock,\n    mainQueue: .unimplemented\n  )\n  viewModel.episode = .mock\n\n  viewModel.favoriteButtonTapped()\n  XCTAssert(viewModel.episode?.isFavorite == true)\n\n  viewModel.favoriteButtonTapped()\n  XCTAssert(viewModel.episode?.isFavorite == false)\n}\n```\n\nWith `.unimplemented`, this test strongly declares that favoriting an episode does not need a scheduler to do the job, which means it is reasonable to assume that the feature is simple and does not involve any asynchrony.\n\nIn the future, should favoriting an episode fire off an API request that involves a scheduler, this test will begin to fail, which is a good thing! This will force us to address the complexity that was introduced. Had we used any other scheduler, it would quietly receive this additional work and the test would continue to pass.\n\n### `UIScheduler`\n\nA scheduler that executes its work on the main queue as soon as possible. This scheduler is inspired by the [equivalent](https://github.com/ReactiveCocoa/ReactiveSwift/blob/58d92aa01081301549c48a4049e215210f650d07/Sources/Scheduler.swift#L92) scheduler in the [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift) project.\n\nIf `UIScheduler.shared.schedule` is invoked from the main thread then the unit of work will be performed immediately. This is in contrast to `DispatchQueue.main.schedule`, which will incur a thread hop before executing since it uses `DispatchQueue.main.async` under the hood.\n\nThis scheduler can be useful for situations where you need work executed as quickly as possible on the main thread, and for which a thread hop would be problematic, such as when performing animations.\n\n### Concurrency APIs\n\nThis library provides `async`-friendly APIs for interacting with Combine schedulers.\n\n```swift\n// Suspend the current task for 1 second\ntry await scheduler.sleep(for: .seconds(1))\n\n// Perform work every 1 second\nfor await instant in scheduler.timer(interval: .seconds(1)) {\n  ...\n}\n``` \n\n### `Publishers.Timer`\n\nA publisher that emits a scheduler's current time on a repeating interval.\n\nThis publisher is an alternative to Foundation's `Timer.publisher`, with its primary difference being that it allows you to use any scheduler for the timer, not just `RunLoop`. This is useful because the `RunLoop` scheduler is not testable in the sense that if you want to write tests against a publisher that makes use of `Timer.publisher` you must explicitly wait for time to pass in order to get emissions. This is likely to lead to fragile tests and greatly bloat the time your tests take to execute.\n\nIt can be used much like Foundation's timer, except you specify a scheduler rather than a run loop:\n\n```swift\nPublishers.Timer(every: .seconds(1), scheduler: DispatchQueue.main)\n  .autoconnect()\n  .sink { print(\"Timer\", $0) }\n```\n\nAlternatively you can call the `timerPublisher` method on a scheduler in order to derive a repeating timer on that scheduler:\n\n```swift\nDispatchQueue.main.timerPublisher(every: .seconds(1))\n  .autoconnect()\n  .sink { print(\"Timer\", $0) }\n```\n\nBut the best part of this timer is that you can use it with `TestScheduler` so that any Combine code you write involving timers becomes more testable. This shows how we can easily simulate the idea of moving time forward 1,000 seconds in a timer:\n\n```swift\nlet scheduler = DispatchQueue.test\nvar output: [Int] = []\n\nPublishers.Timer(every: 1, scheduler: scheduler)\n  .autoconnect()\n  .sink { _ in output.append(output.count) }\n  .store(in: \u0026self.cancellables)\n\nXCTAssertEqual(output, [])\n\nscheduler.advance(by: 1)\nXCTAssertEqual(output, [0])\n\nscheduler.advance(by: 1)\nXCTAssertEqual(output, [0, 1])\n\nscheduler.advance(by: 1_000)\nXCTAssertEqual(output, Array(0...1_001))\n```\n\n## Compatibility\n\nThis library is compatible with iOS 13.2 and higher. Please note that there are bugs in the Combine framework and iOS 13.1 and lower that will cause crashes when trying to compare `DispatchQueue.SchedulerTimeType` values, which is an operation that the `TestScheduler` depends on.\n\n## Installation\n\nYou can add CombineSchedulers to an Xcode project by adding it as a package dependency.\n\n  1. From the **File** menu, select **Swift Packages › Add Package Dependency…**\n  2. Enter \"https://github.com/pointfreeco/combine-schedulers\" into the package repository URL text field\n  3. Depending on how your project is structured:\n      - If you have a single application target that needs access to the library, then add **CombineSchedulers** directly to your application.\n      - If you want to use this library from multiple targets you must create a shared framework that depends on **CombineSchedulers**, and then depend on that framework from your other targets.\n\n## Documentation\n\nThe latest documentation for Combine Schedulers' APIs is available [here](https://pointfreeco.github.io/combine-schedulers/).\n\n## Other Libraries\n\n* [Entwine](https://github.com/tcldr/Entwine)\n\n## License\n\nThis library is released under the MIT license. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fd-date%2Fcombine-schedulers-android","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fd-date%2Fcombine-schedulers-android","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fd-date%2Fcombine-schedulers-android/lists"}