Coder Social home page Coder Social logo

pointfreeco / combine-schedulers Goto Github PK

View Code? Open in Web Editor NEW
684.0 15.0 91.0 427 KB

⏰ A few schedulers that make working with Combine more testable and more versatile.

Home Page: https://www.pointfree.co

License: MIT License

Swift 99.05% Makefile 0.95%
combine-framework testing

combine-schedulers's Introduction

⏰ Combine Schedulers

CI

A few schedulers that make working with Combine more testable and more versatile.

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, a video series exploring functional programming and Swift hosted by Brandon Williams and Stephen Celis.

You can watch all of the episodes here.

video poster image

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:

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

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

Then 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.

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:

func race<Output, Failure: Error>(
  _ first: Future<Output, Failure>,
  _ second: Future<Output, Failure>
) -> AnyPublisher<Output, Failure> {
  first
    .merge(with: second)
    .prefix(1)
    .eraseToAnyPublisher()
}

Although this publisher is quite simple we may still want to write some tests for it.

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

let scheduler = DispatchQueue.test

let first = Future<Int, Never> { callback in
  scheduler.schedule(after: scheduler.now.advanced(by: 1)) { callback(.success(1)) }
}
let second = Future<Int, Never> { callback in
  scheduler.schedule(after: scheduler.now.advanced(by: 2)) { callback(.success(2)) }
}

And then we can race these futures and collect their emissions into an array:

var output: [Int] = []
let cancellable = race(first, second).sink { output.append($0) }

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

scheduler.advance(by: 1)
XCTAssertEqual(output, [1])

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

scheduler.advance(by: 1)
XCTAssertEqual(output, [1])

This 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.

ImmediateScheduler

The 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.

So, 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.

This 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.

This 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.

As a basic example, suppose you have a view model that loads some data after waiting for 10 seconds from when a button is tapped:

class HomeViewModel: ObservableObject {
  @Published var episodes: [Episode]?

  let apiClient: ApiClient

  init(apiClient: ApiClient) {
    self.apiClient = apiClient
  }

  func reloadButtonTapped() {
    Just(())
      .delay(for: .seconds(10), scheduler: DispatchQueue.main)
      .flatMap { apiClient.fetchEpisodes() }
      .assign(to: &self.$episodes)
  }
}

In order to test this code you would literally need to wait 10 seconds for the publisher to emit:

func testViewModel() {
  let viewModel = HomeViewModel(apiClient: .mock)

  viewModel.reloadButtonTapped()

  _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 10)

  XCTAssert(viewModel.episodes, [Episode(id: 42)])
}

Alternatively, we can explicitly pass a scheduler into the view model initializer so that it can be controller from the outside:

class HomeViewModel: ObservableObject {
  @Published var episodes: [Episode]?

  let apiClient: ApiClient
  let scheduler: AnySchedulerOf<DispatchQueue>

  init(apiClient: ApiClient, scheduler: AnySchedulerOf<DispatchQueue>) {
    self.apiClient = apiClient
    self.scheduler = scheduler
  }

  func reloadButtonTapped() {
    Just(())
      .delay(for: .seconds(10), scheduler: self.scheduler)
      .flatMap { self.apiClient.fetchEpisodes() }
      .assign(to: &self.$episodes)
  }
}

And then in tests use an immediate scheduler:

func testViewModel() {
  let viewModel = HomeViewModel(
    apiClient: .mock,
    scheduler: .immediate
  )

  viewModel.reloadButtonTapped()

  // No more waiting...

  XCTAssert(viewModel.episodes, [Episode(id: 42)])
}

Animated schedulers

CombineSchedulers comes with helpers that aid in asynchronous animations in both SwiftUI and UIKit.

If 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.

For example, to animate an API response in your view model, you can specify that the scheduler that receives this state should be animated:

self.apiClient.fetchEpisode()
  .receive(on: self.scheduler.animation())
  .assign(to: &self.$episode)

If you are powering a UIKit feature with Combine, you can use the .animate method, which mirrors UIView.animate:

self.apiClient.fetchEpisode()
  .receive(on: self.scheduler.animate(withDuration: 0.3))
  .assign(to: &self.$episode)

UnimplementedScheduler

A scheduler that causes a test to fail if it is used.

This scheduler can provide an additional layer of certainty that a tested code path does not require the use of a scheduler.

As 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.

For example, the following view model has a couple responsibilities:

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

  let apiClient: ApiClient
  let mainQueue: AnySchedulerOf<DispatchQueue>

  init(apiClient: ApiClient, mainQueue: AnySchedulerOf<DispatchQueue>) {
    self.apiClient = apiClient
    self.mainQueue = mainQueue
  }

  func reloadButtonTapped() {
    self.apiClient.fetchEpisode()
      .receive(on: self.mainQueue)
      .assign(to: &self.$episode)
  }

  func favoriteButtonTapped() {
    self.episode?.isFavorite.toggle()
  }
}
  • It lets the user tap a button to refresh some episode data
  • It lets the user toggle if the episode is one of their favorites

The API client delivers the episode on a background queue, so the view model must receive it on its main queue before mutating its state.

Tapping the favorite button, however, involves no scheduling. This means that a test can be written with an unimplemented scheduler:

func testFavoriteButton() {
  let viewModel = EpisodeViewModel(
    apiClient: .mock,
    mainQueue: .unimplemented
  )
  viewModel.episode = .mock

  viewModel.favoriteButtonTapped()
  XCTAssert(viewModel.episode?.isFavorite == true)

  viewModel.favoriteButtonTapped()
  XCTAssert(viewModel.episode?.isFavorite == false)
}

With .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.

In 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.

UIScheduler

A scheduler that executes its work on the main queue as soon as possible. This scheduler is inspired by the equivalent scheduler in the ReactiveSwift project.

If 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.

This 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.

Concurrency APIs

This library provides async-friendly APIs for interacting with Combine schedulers.

// Suspend the current task for 1 second
try await scheduler.sleep(for: .seconds(1))

// Perform work every 1 second
for await instant in scheduler.timer(interval: .seconds(1)) {
  ...
}

Publishers.Timer

A publisher that emits a scheduler's current time on a repeating interval.

This 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.

It can be used much like Foundation's timer, except you specify a scheduler rather than a run loop:

Publishers.Timer(every: .seconds(1), scheduler: DispatchQueue.main)
  .autoconnect()
  .sink { print("Timer", $0) }

Alternatively you can call the timerPublisher method on a scheduler in order to derive a repeating timer on that scheduler:

DispatchQueue.main.timerPublisher(every: .seconds(1))
  .autoconnect()
  .sink { print("Timer", $0) }

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

let scheduler = DispatchQueue.test
var output: [Int] = []

Publishers.Timer(every: 1, scheduler: scheduler)
  .autoconnect()
  .sink { _ in output.append(output.count) }
  .store(in: &self.cancellables)

XCTAssertEqual(output, [])

scheduler.advance(by: 1)
XCTAssertEqual(output, [0])

scheduler.advance(by: 1)
XCTAssertEqual(output, [0, 1])

scheduler.advance(by: 1_000)
XCTAssertEqual(output, Array(0...1_001))

Compatibility

This 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.

Installation

You can add CombineSchedulers to an Xcode project by adding it as a package dependency.

  1. From the File menu, select Swift Packages › Add Package Dependency…
  2. Enter "https://github.com/pointfreeco/combine-schedulers" into the package repository URL text field
  3. Depending on how your project is structured:
    • If you have a single application target that needs access to the library, then add CombineSchedulers directly to your application.
    • 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.

Documentation

The latest documentation for Combine Schedulers' APIs is available here.

Other Libraries

License

This library is released under the MIT license. See LICENSE for details.

combine-schedulers's People

Contributors

brianmichel avatar emixb avatar freak4pc avatar iampatbrown avatar john-flanagan avatar junpluse avatar maximkrouk avatar mbrandonw avatar mrs- avatar rlziii avatar stephencelis avatar ytyubox avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

combine-schedulers's Issues

Install using Carthage

Currently this can only be installed with SPM, it'd be nice if it could be installed with Carthage as well.

Would that be a PR you'd be willing to accept? I was thinking it could be done in a similar way to how swift-snapshot-testing does it (with xcodegen creating the project).

Cannot find Publishers

I am using Xcode 14.1, minimum deployments iOS 15.5. added CombineSchedulers via SPM, and import CombineSchedulers in my swift file.

But when I am using Publishers.Timer(every: .second(1), scheduler: scheduler) API, I am getting Xcode error saying Cannot find Publishers in scope.

Is there anything I missed from the documentation? Thanks in advance for your help.

iOS 13 EXC_BAD_INSTRUCTION

Describe the bug
This problem is only related to iOS 13 (on iOS 14 and 15 everything is working fine) and maybe it's a bug in Combine itself. When the crash is happening action is still performed on the main thread (check screenshot section).

To Reproduce
Actually hard to reproduce, in the case of our project it happens in tree cases (maybe more but we just don't figure out every one of them yet):

  1. When performing observing of outputVolume:
  // ...

  extension Publisher where Failure == Never {
    @inlinable
    public func sinkValues(_ valueReceiver: @escaping (Output) -> Void) -> AnyCancellable {
      return sink(receiveValue: valueReceiver)
    }
  }	

  // ...	

  // this code is called from application(_:didFinishLaunchingWithOptions)
  try? AVAudioSession.sharedInstance().setCategory(.playback)
  AVAudioSession.sharedInstance().publisher(for: \.outputVolume)
    .receive(on: UIScheduler.shared) // crash happening here
    .sinkValues { _ in
      EduDoVideoPlayerPreferenceProvider.global.isMuted = false
    }
    .store(in: &soundManagerSubscriptions)
  1. Force unwrapping dequeued cell, that contains the view that does this:
  private func observeState() {
    $state
      .receive(on: UIScheduler.shared) // crash happening here
      .sinkValues { [weak self] state in self?.configureView(using: state) })
      .store(in: &subscriptions)
  }
  1. Observing composable state changes using .receive(on: UIScheduler.shared). This happens not every time, sometimes it works fine and sometimes it crashes.

Can't share all code due to NDA.

In all cases, the error is the same.

Expected behavior
App is not crashing 😓.

Screenshots
Reversed stack trace screenshots.

Breakpoint prints the result of Thread.isMainThread and when it crashes it's still the main thread.
photo_2021-09-29 13 40 44

photo_2021-09-29 13 40 48

photo_2021-09-29 13 40 57

Environment

  • Xcode 12.5.1
  • Swift 5.4.2
  • iOS 13

Release builds can't find XCTFail

Describe the bug
This has 0.1.0 of xctest-dynamic-overlay as a dependency. Release builds can now fail with the following error

Cannot find 'XCTFail' in scope

The version for xctest-dynamic-overlay should probably be updated to >= 0.2 since 0.1 doesn't define XCTFail in release builds and FailableScheduler now exists in release builds.

To Reproduce
Build the repo using version 0.1.0 of xctest-dynamic-overlay. Easiest way I know to reproduce it is with the following with will use the version in the Package.resolved.

swift build --disable-automatic-resolution --configuration release

Expected behavior
It should build

Environment

  • Xcode 12.5.1
  • Swift 5.4

ImmediateScheduler not returning immediately with debounce

Describe the bug
I am trying to use ImmediateScheduler to avoid using XCTWaiter to wait for a given debounce to finish. However, when I pass in a immediate scheduler to debounce() the value assigned by the subscriber is not set before the assertion happens. It won't even work if I add _ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 5) even though the debounce is only for 0.5 seconds.

The test succeeds if I use DispatchQueue.main and wait for 0.5 seconds. It might just be me understanding the functionality of purposes for ImmediateScheduler

// I am passing in a scheduler variable normally instead of hardcoding, just for demonstration purposes.
usernameState
            .debounce(for: 0.5, scheduler: DispatchQueue.immediateScheduler)
            .receive(on: DispatchQueue.immediateScheduler)
            .map { $0.message }
            .assign(to: \.usernameError, on: self)
            .store(in: &cancellables)

// Test:
func testUsernameErrorMessage() throws {
        var usernameError = ""
        
        viewModel.$usernameError
            .sink(receiveValue: { usernameError = $0 })
            .store(in: &cancellables)

        // Empty state
        viewModel.username = ""
        XCTAssertEqual(usernameError, "my error message")
}

Expected behavior
I expected the publisher to return the value immediately instead of having to wait, however it won't even work with an ImmediateScheduler if I add XCTWaiter.

Environment

  • Xcode 12 beta 6
  • Swift 5.3
  • OS (if applicable): iOS 14

Any way to convert AnySchedulerOf<RunLoop> back to RunLoop?

Describe the bug
Not a bug
Hi guys, I'm using CADisplayLink in my project and want to inject AnyScheduler to test the behavior. However, CADisplayLink takes in RunLoop instead of Scheduler as a parameter to add(to:,forMode:) method.
So I cannot use AnySchedulerOf that I injected.

Is there a way to convert RunLoop.main.eraseToAnyScheduler() back to RunLoop type? Is it possible?

To Reproduce

// And/or enter code that reproduces the behavior here.
private let mainLoop: AnySchedulerOf<RunLoop>

let displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink.add(to: self.mainLoop, forMode: .default) // <- Error Above

Expected behavior

Screenshots
Screen Shot 2021-09-07 at 12 32 11 PM

Environment

  • Xcode 12.5.1
  • Swift 5.3
  • OS (if applicable): [e.g. iOS 13]

Additional context
Add any more context about the problem here.

EXC_BAD_ACCESS crash with iOS 15.2

Describe the bug
When adding a type erased dispatch queue to my composable architecture environment, I found a run time error that occurs when the queue is erased to AnyScheduler. The code builds successfully, but as soon as it tries to load the app, it crashes, pointing to the "eraseToAnyScheduler()" call. This also occurs if I try to type erase locally with a labeled queue.

To Reproduce
schedularTest.zip

public struct HomeEnvironment {
  public var mainQueue: AnySchedulerOf<DispatchQueue>

  public init(
    mainQueue: AnySchedulerOf<DispatchQueue>
  ) {
    self.mainQueue = mainQueue
  }
}

@main
struct schedularTestApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView(
        store: .init(
          initialState: HomeState(),
          reducer: homeReducer,
          environment: HomeEnvironment(mainQueue: .main)
        )
      )
    }
  }
}

Expected behavior
Scheduler is successfully erased and the app doesn't crash 🥲

Screenshots
image

Environment

  • Xcode 13.2
  • Swift 5
  • OS: iOS 15.2

Additional context
This bug was initially found when the preview would crash with no feedback. I then built out a specific preview target to try and run on the simulator. This also crashed, but gave me a stack trace that pointed to the erasing of the scheduler. Because my project was pretty intense, I created a new small test project to see if it was my project or the framework (see zipped project). Even in just this small app that only imports the latest release of the composable architecture, the bug still persists. I then tested on a device, and the bug was not there. My device was running iOS 15.1. I downloaded the 15.0 simulators and those do not have this bug either. It only appears to occur with iOS 15.2.

Build documentation on Xcode14b5 for projects including `CombineSchedulers` fail

Describe the bug
Only as a heads up, when trying to build documentation on Use Xcode14 beta 5 fails with not found symbol identifiers.

To Reproduce
Use Xcode14 beta 5 with a project that has the CombineSchedulers dependency and run "Build Documentation" from the Product menu

Expected behavior
Documentation builds and DocC is exportable

Screenshots
Screenshot 2022-08-15 at 11 29 09

Environment

  • Xcode 14.0 beta 5
  • Swift 5.7

cocoapods spec not found

Describe the bug
cocoapods spec is missing when running pod install

[!] Unable to find a specification for `CombineSchedulers`

To Reproduce
run pod repo update or pod install --repo-update, then do a pod search CombineSchedulers, you will see:

[!] Unable to find a pod with name, author, summary, or description matching `CombineSchedulers`

Expected behavior
CombineSchedulers should be searchable in pod repo, and pod install should not fail if include pod CombineSchedulers.

Environment

  • Xcode [e.g. 14.3]
  • Swift [e.g. 5.5]
  • OS (if applicable): [e.g. iOS 16]

Simulate the inaccuracy of schedulers

This is a fantastic library and aids testing of time based asynchrony really well. But in a way, it only simulates the happiest path due to the precision of the scheduler. As we all know, when a timer is set it will complete at a point of timer after the scheduled time, if only a fraction of a second after. In your videos on Clocks, you demonstrate how this drift can add relatively quickly.

Would it be possible to add a feature where we can test against these small inaccuracies? So after advancing the scheduler, rather than the schedulers' .now property being set exactly to the next scheduled actions date, it could be set with a defined drift, maybe even the .minimumTolerance. This way, if we need to, we can test against these small inaccuracies of timing that can add up after many events.

Can't get version 0.7.0 compile on Xcode 14 beta 1

Describe the bug
Thank you for the concurrency work!

Hoever I got a compile error when try to use latest TCA package 0.39.0, which depends on this package at version 0.7.0.

AnyScheduler.swift:333:48: error build: Protocol type with type arguments can only be used as a generic constraint

To Reproduce
Download the package, and compile.

Expected behavior
It should compile.

Screenshots
image

Environment

  • Xcode 14.0 beta 1
  • Swift 5.7
  • OS iOS 16 beta(simulator)
  • macOS 12.4 Monterey

Cannot archive build on Xcode 13 beta 3

Describe the bug

When running archive build on Xcode 13 beta 3, the build fail since it depends on Combine.framework built by different compiler version.

Failed to build module 'Combine'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 5.5 (swiftlang-1300.0.24.14 clang-1300.0.25.10)', while this compiler is 'Apple Swift version 5.5 (swiftlang-1300.0.24.13 clang-1300.0.25.10)'). Please select a toolchain which matches the SDK.

Screenshots
Screen Shot 2021-07-15 at 13 36 50

Environment

  • Xcode 13 beta 3
  • Swift 5.5

UIScheduler SchedulerOptions type?

Why does the UIScheduler have public typealias SchedulerOptions = Never? It does not seem to fit to an API with AnySchedulerOf<DispatchQueue> because the SchedulerOptions types do not match. Or am I supposed to use AnySchedulerOf<UIScheduler> for the API type if I want to also enable use of test scheduler? But then I can not replace the UIScheduler with DispatchQueue.main.

I'm just wondering this because in ReactiveSwift the UIScheduler conforms to their own Scheduler protocol (immediate scheduling only) and DispatchQueue conforms to DateScheduler: Scheduler protocol (can schedule to the future). So if the API defines var scheduler: Scheduler both UIScheduler and DispatchQueue.main can be used.

I'm not even sure if there is any use case to actually replace UIScheduler with plain DispatchQueue. I was just used to writing this kind of API 😅

Subscriber does not receive values when `.subscribe(on: scheduler).receive(on: scheduler)` used with the TestScheduler

Describe the bug

I looks like there is an issue where a subscriber does not receive values when test scheduler is used in combination with subscribe(on:) and receive(on:) operators.

To Reproduce

    @MainActor func test_stuck() async {
        let subject = PassthroughSubject<Int, Never>()
        let scheduler = DispatchQueue.test
        var values: [Int] = []
        
        let cancelable = subject
            .subscribe(on: scheduler)
            .receive(on: scheduler)
            .map { $0 * $0 }
            .receive(on: scheduler)
            .sink { value in values.append(value) }
        
        let valuesToSend = [0, 1, 2, 3, 4]
        
        for value in valuesToSend {
            subject.send(value)
        }
        await scheduler.run()
        XCTAssertEqual(values, [0, 1, 4, 9, 16])
    }

Expected behavior
The above test should pass

Screenshots
N/A

Environment

  • Xcode 13.4.1
  • Swift 5.6.1

When compiled with library evolution turned on, the resulting .swiftinterface file is not compiling.

Describe the bug

This is probably a Swift compiler limitation/bug, which can be easily overcome with a simple workaround - see below.

To Reproduce

To compile with library evolution, you use the following command line:

xcrun xcodebuild -workspace "./.swiftpm/xcode/package.xcworkspace" -scheme "combine-schedulers" -configuration Debug -destination "generic/platform=iOS Simulator" -sdk iphonesimulator BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO CLANG_ENABLE_CODE_COVERAGE=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=NO SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO "OTHER_SWIFT_FLAGS=-Xfrontend -no-serialize-debugging-options"

You can then inspect the resulting .swiftinterface file by looking in the PRODUCTS folder, in CombineSchedulers.swiftmodule

There you will see files like arm64-apple-ios-simulator.swiftinterface or x86_64-apple-ios-simulator.swiftinterface

The gist of the problem is the following line (235) in the file Timer.swift:

public func receive<S: Subscriber>(subscriber: S)
      where Failure == S.Failure, Output == S.Input {

The compiler gets confused because the generic parameter 'S' in the receive function has the same name as the generic parameter 'S' in
public final class Timer<S: Scheduler>: ConnectablePublisher
(see line 83)

The Swift compiler gets confused because inside the .swiftinterface file nested types get fully qualified, so for receive you will get something like this:

    final public func receive<S>(subscriber: S) where S : Combine.Subscriber, S.SchedulerTimeType == S.Input, S.Failure == Swift.Never

And this is definitely confusing for the compiler.

The way to fix it is to rename the parameter 'S' in the receive to something else, e.g. 'SC':

public func receive<SC: Subscriber>(subscriber: SC)
      where Failure == SC.Failure, Output == SC.Input 

In that case the resulting .swiftinterface will get this:

final public func receive<SC>(subscriber: SC) where SC : Combine.Subscriber, S.SchedulerTimeType == SC.Input, SC.Failure == Swift.Never

Since it's a very easy fix, I would greatly appreciate if it's done in a following minor/patch release.

Compatibility with M1 macbook (arm64)

Describe the bug
I cannot build the tests on an M1 macbook. It seems to work fine on intel machine.

The issue is that if I start with a clean project I get No such module CombineSchedulers in IDE where the import CombineSchedulers is and when building my tests - under Compile Swift Source Files (arm64) I also get the error No such module CombineSchedulers.

If I build my app target first, the errors in IDE disappears. But then I proceed to building my tests and I get No such module MYMODULE under Compile Swift Source Files (arm64).

Expected behavior
Test target should build and run tests.

Environment

  • Xcode 12.3
  • Swift 5.0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.