diff --git a/Sources/Dependencies/ConcurrencySupport/Scheduler.swift b/Sources/Dependencies/ConcurrencySupport/Scheduler.swift new file mode 100644 index 00000000..e5027ada --- /dev/null +++ b/Sources/Dependencies/ConcurrencySupport/Scheduler.swift @@ -0,0 +1,25 @@ +#if canImport(Combine) + import Combine + + extension Scheduler { + /// Propagates dependencies across the scheduler boundary. + /// + /// - Parameter update: Enables transforming the propagated dependencies. No-ops by default. + /// - Returns: A version of the scheduler that propagates dependencies. + public func dependencies( + _ update: @escaping (inout DependencyValues) -> Void = { _ in } + ) -> AnySchedulerOf { + func forward(_ action: @escaping () -> Void) -> () -> Void { + let continuation = withDependencies(update) { withEscapedDependencies { $0 } } + return { continuation.yield(action) } + } + return AnyScheduler( + minimumTolerance: { self.minimumTolerance }, + now: { self.now }, + scheduleImmediately: { self.schedule(options: $0, forward($1)) }, + delayed: { self.schedule(after: $0, tolerance: $1, options: $2, forward($3)) }, + interval: { self.schedule(after: $0, interval: $1, tolerance: $2, options: $3, forward($4)) } + ) + } + } +#endif diff --git a/Sources/Dependencies/DependencyValues/MainQueue.swift b/Sources/Dependencies/DependencyValues/MainQueue.swift index e3279cd1..2bf815d8 100644 --- a/Sources/Dependencies/DependencyValues/MainQueue.swift +++ b/Sources/Dependencies/DependencyValues/MainQueue.swift @@ -6,8 +6,9 @@ /// /// Introduce controllable timing to your features by using the ``Dependency`` property wrapper /// with a key path to this property. The wrapped value is a Combine scheduler with the time - /// type and options of a dispatch queue. By default, `DispatchQueue.main` will be provided, - /// with the exception of XCTest cases, in which an "unimplemented" scheduler will be provided. + /// type and options of a dispatch queue. By default, a variant of `DispatchQueue.main` that + /// forwards dependencies will be provided, with the exception of XCTest cases, in which an + /// "unimplemented" scheduler will be provided. /// /// For example, you could introduce controllable timing to an observable object model that /// counts the number of seconds it's onscreen: @@ -53,7 +54,7 @@ } private enum MainQueueKey: DependencyKey { - static let liveValue = AnySchedulerOf.main + static let liveValue = DispatchQueue.main.dependencies() static let testValue = AnySchedulerOf .unimplemented(#"@Dependency(\.mainQueue)"#) } diff --git a/Tests/DependenciesTests/SchedulerTests.swift b/Tests/DependenciesTests/SchedulerTests.swift new file mode 100644 index 00000000..5e3b95ec --- /dev/null +++ b/Tests/DependenciesTests/SchedulerTests.swift @@ -0,0 +1,53 @@ +#if canImport(Combine) + import Dependencies + import Dispatch + import XCTest + + final class SchedulerTests: XCTestCase { + func testDependencyPropagation() { + // we have to use live schedulers here because a test scheduler would + // propagate dependencies anyway, since it's immediate. + let queue = DispatchQueue.global(qos: .userInteractive) + let scheduler1 = queue.dependencies() + let scheduler2 = queue.dependencies { $0.int = 7 } + + var value1a, value1b, value2: Int? + let expectation = self.expectation(description: "schedulers") + expectation.expectedFulfillmentCount = 3 + + @Dependency(\.int) var int + scheduler1.schedule { + value1a = int + expectation.fulfill() + } + withDependencies { + $0.int = 5 + } operation: { + scheduler1.schedule { + value1b = int + expectation.fulfill() + } + scheduler2.schedule { + value2 = int + expectation.fulfill() + } + } + + self.wait(for: [expectation], timeout: 1) + XCTAssertEqual(value1a, 42) + XCTAssertEqual(value1b, 5) + XCTAssertEqual(value2, 7) + } + } + + extension DependencyValues { + fileprivate var int: Int { + get { self[IntKey.self] } + set { self[IntKey.self] = newValue } + } + } + + private enum IntKey: TestDependencyKey { + static let testValue = 42 + } +#endif