diff --git a/Examples/MotionManager/MotionManager/MotionManagerView.swift b/Examples/MotionManager/MotionManager/MotionManagerView.swift index de20c8e1a708..bb75fcc0a31a 100644 --- a/Examples/MotionManager/MotionManager/MotionManagerView.swift +++ b/Examples/MotionManager/MotionManager/MotionManagerView.swift @@ -146,9 +146,10 @@ struct AppView_Previews: PreviewProvider { userAcceleration: .init(x: -cos(-3 * t), y: sin(2 * t), z: -cos(t)) ) ) - } - .eraseToEffect() - }, + } + .setFailureType(to: MotionClient.Error.self) + .eraseToEffect() + }, startDeviceMotionUpdates: { _ in .fireAndForget { isStarted = true } }, stopDeviceMotionUpdates: { _ in .fireAndForget { isStarted = false } } ) diff --git a/Package.swift b/Package.swift index b9e283e5fb42..a34fad259669 100644 --- a/Package.swift +++ b/Package.swift @@ -21,18 +21,21 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.1.1") + .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.1.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.1.1"), ], targets: [ .target( name: "ComposableArchitecture", dependencies: [ - "CasePaths" + "CasePaths", + "CombineSchedulers", ] ), .testTarget( name: "ComposableArchitectureTests", dependencies: [ + "CombineSchedulers", "ComposableArchitecture", ] ), diff --git a/Sources/ComposableArchitecture/Effects/Timer.swift b/Sources/ComposableArchitecture/Effects/Timer.swift index df9502b1dd9d..c59a7780ce1c 100644 --- a/Sources/ComposableArchitecture/Effects/Timer.swift +++ b/Sources/ComposableArchitecture/Effects/Timer.swift @@ -1,7 +1,7 @@ import Combine -import Foundation +import CombineSchedulers -extension Effect { +extension Effect where Failure == Never { /// Returns an effect that repeatedly emits the current time of the given /// scheduler on the given interval. /// @@ -35,24 +35,10 @@ extension Effect { options: S.SchedulerOptions? = nil ) -> Effect where S: Scheduler, S.SchedulerTimeType == Output { - Deferred { () -> Publishers.HandleEvents> in - let subject = PassthroughSubject() - - let cancellable = scheduler.schedule( - after: scheduler.now.advanced(by: interval), - interval: interval, - tolerance: tolerance ?? .seconds(.max), - options: options - ) { - subject.send(scheduler.now) - } - - return subject.handleEvents( - receiveCompletion: { _ in cancellable.cancel() }, - receiveCancel: cancellable.cancel - ) - } - .eraseToEffect() - .cancellable(id: id) + Publishers.Timer(every: interval, tolerance: tolerance, scheduler: scheduler, options: options) + .autoconnect() + .setFailureType(to: Failure.self) + .eraseToEffect() + .cancellable(id: id) } } diff --git a/Sources/ComposableArchitecture/Internal/Exports.swift b/Sources/ComposableArchitecture/Internal/Exports.swift index 55ecc589b5af..6a2acee1484b 100644 --- a/Sources/ComposableArchitecture/Internal/Exports.swift +++ b/Sources/ComposableArchitecture/Internal/Exports.swift @@ -1 +1,2 @@ @_exported import CasePaths +@_exported import CombineSchedulers diff --git a/Sources/ComposableArchitecture/Scheduling/AnyScheduler.swift b/Sources/ComposableArchitecture/Scheduling/AnyScheduler.swift deleted file mode 100644 index 372488250516..000000000000 --- a/Sources/ComposableArchitecture/Scheduling/AnyScheduler.swift +++ /dev/null @@ -1,99 +0,0 @@ -import Combine -import Foundation - -/// A type-erasing scheduler that defines when and how to execute a closure. -public struct AnyScheduler: Scheduler -where - SchedulerTimeType: Strideable, - SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible -{ - - private let _minimumTolerance: () -> SchedulerTimeType.Stride - private let _now: () -> SchedulerTimeType - private let _scheduleAfterIntervalToleranceSchedulerOptionsAction: - ( - SchedulerTimeType, - SchedulerTimeType.Stride, - SchedulerTimeType.Stride, - SchedulerOptions?, - @escaping () -> Void - ) -> Cancellable - private let _scheduleAfterToleranceSchedulerOptionsAction: - ( - SchedulerTimeType, - SchedulerTimeType.Stride, - SchedulerOptions?, - @escaping () -> Void - ) -> Void - private let _scheduleSchedulerOptionsAction: (SchedulerOptions?, @escaping () -> Void) -> Void - - /// The minimum tolerance allowed by the scheduler. - public var minimumTolerance: SchedulerTimeType.Stride { self._minimumTolerance() } - - /// This scheduler’s definition of the current moment in time. - public var now: SchedulerTimeType { self._now() } - - let scheduler: Any - - /// Creates a type-erasing scheduler to wrap the provided scheduler. - /// - /// - Parameters: - /// - scheduler: A scheduler to wrap with a type-eraser. - public init( - _ scheduler: S - ) - where - S: Scheduler, S.SchedulerTimeType == SchedulerTimeType, S.SchedulerOptions == SchedulerOptions - { - self.scheduler = scheduler - self._now = { scheduler.now } - self._minimumTolerance = { scheduler.minimumTolerance } - self._scheduleAfterToleranceSchedulerOptionsAction = scheduler.schedule - self._scheduleAfterIntervalToleranceSchedulerOptionsAction = scheduler.schedule - self._scheduleSchedulerOptionsAction = scheduler.schedule - } - - /// Performs the action at some time after the specified date. - public func schedule( - after date: SchedulerTimeType, - tolerance: SchedulerTimeType.Stride, - options: SchedulerOptions?, - _ action: @escaping () -> Void - ) { - self._scheduleAfterToleranceSchedulerOptionsAction(date, tolerance, options, action) - } - - /// Performs the action at some time after the specified date, at the - /// specified frequency, taking into account tolerance if possible. - public func schedule( - after date: SchedulerTimeType, - interval: SchedulerTimeType.Stride, - tolerance: SchedulerTimeType.Stride, - options: SchedulerOptions?, - _ action: @escaping () -> Void - ) -> Cancellable { - self._scheduleAfterIntervalToleranceSchedulerOptionsAction( - date, interval, tolerance, options, action) - } - - /// Performs the action at the next possible opportunity. - public func schedule( - options: SchedulerOptions?, - _ action: @escaping () -> Void - ) { - self._scheduleSchedulerOptionsAction(options, action) - } -} - -/// A convenience type to specify an `AnyScheduler` by the scheduler it wraps rather than by the -/// time type and options type. -public typealias AnySchedulerOf = AnyScheduler< - Scheduler.SchedulerTimeType, Scheduler.SchedulerOptions -> where Scheduler: Combine.Scheduler - -extension Scheduler { - /// Wraps this scheduler with a type eraser. - public func eraseToAnyScheduler() -> AnyScheduler { - AnyScheduler(self) - } -} diff --git a/Sources/ComposableArchitecture/Scheduling/TestScheduler.swift b/Sources/ComposableArchitecture/Scheduling/TestScheduler.swift deleted file mode 100644 index ee17301977e3..000000000000 --- a/Sources/ComposableArchitecture/Scheduling/TestScheduler.swift +++ /dev/null @@ -1,135 +0,0 @@ -import Combine -import Foundation - -/// A scheduler whose current time and execution can be controlled in a deterministic manner. -public final class TestScheduler: Scheduler -where SchedulerTimeType: Strideable, SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible { - - private var lastSequence: UInt = 0 - public let minimumTolerance: SchedulerTimeType.Stride = .zero - public private(set) var now: SchedulerTimeType - private var scheduled: [(sequence: UInt, date: SchedulerTimeType, action: () -> Void)] = - [] - - /// Creates a test scheduler with the given date. - /// - /// - Parameter now: The current date of the test scheduler. - public init(now: SchedulerTimeType) { - self.now = now - } - - /// Advances the scheduler by the given stride. - /// - /// - Parameter stride: A stride. - public func advance(by stride: SchedulerTimeType.Stride = .zero) { - let finalDate = self.now.advanced(by: stride) - - while self.now <= finalDate { - self.scheduled.sort { ($0.date, $0.sequence) < ($1.date, $1.sequence) } - - guard - let nextDate = self.scheduled.first?.date, - finalDate >= nextDate - else { - self.now = finalDate - return - } - - self.now = nextDate - - while let (_, date, action) = self.scheduled.first, date == nextDate { - self.scheduled.removeFirst() - action() - } - } - } - - /// Runs the scheduler until it has no scheduled items left. - public func run() { - while let date = self.scheduled.first?.date { - self.advance(by: self.now.distance(to: date)) - } - } - - public func schedule( - after date: SchedulerTimeType, - interval: SchedulerTimeType.Stride, - tolerance _: SchedulerTimeType.Stride, - options _: SchedulerOptions?, - _ action: @escaping () -> Void - ) -> Cancellable { - let sequence = self.nextSequence() - - func scheduleAction(for date: SchedulerTimeType) -> () -> Void { - return { [weak self] in - let nextDate = date.advanced(by: interval) - self?.scheduled.append((sequence, nextDate, scheduleAction(for: nextDate))) - action() - } - } - - self.scheduled.append((sequence, date, scheduleAction(for: date))) - - return AnyCancellable { [weak self] in - self?.scheduled.removeAll(where: { $0.sequence == sequence }) - } - } - - public func schedule( - after date: SchedulerTimeType, - tolerance _: SchedulerTimeType.Stride, - options _: SchedulerOptions?, - _ action: @escaping () -> Void - ) { - self.scheduled.append((self.nextSequence(), date, action)) - } - - public func schedule(options _: SchedulerOptions?, _ action: @escaping () -> Void) { - self.scheduled.append((self.nextSequence(), self.now, action)) - } - - private func nextSequence() -> UInt { - self.lastSequence += 1 - return self.lastSequence - } -} - -extension Scheduler -where - SchedulerTimeType == DispatchQueue.SchedulerTimeType, - SchedulerOptions == DispatchQueue.SchedulerOptions -{ - /// A test scheduler of dispatch queues. - public static var testScheduler: TestSchedulerOf { - // NB: `DispatchTime(uptimeNanoseconds: 0) == .now())`. Use `1` for consistency. - TestScheduler(now: SchedulerTimeType(DispatchTime(uptimeNanoseconds: 1))) - } -} - -extension Scheduler -where - SchedulerTimeType == RunLoop.SchedulerTimeType, - SchedulerOptions == RunLoop.SchedulerOptions -{ - /// A test scheduler of run loops. - public static var testScheduler: TestSchedulerOf { - TestScheduler(now: SchedulerTimeType(Date(timeIntervalSince1970: 0))) - } -} - -extension Scheduler -where - SchedulerTimeType == OperationQueue.SchedulerTimeType, - SchedulerOptions == OperationQueue.SchedulerOptions -{ - /// A test scheduler of operation queues. - public static var testScheduler: TestSchedulerOf { - TestScheduler(now: SchedulerTimeType(Date(timeIntervalSince1970: 0))) - } -} - -/// A convenience type to specify a `TestScheduler` by the scheduler it wraps rather than by the -/// time type and options type. -public typealias TestSchedulerOf = TestScheduler< - Scheduler.SchedulerTimeType, Scheduler.SchedulerOptions -> where Scheduler: Combine.Scheduler diff --git a/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift index 9f7b2b054fcf..64d02934d0c3 100644 --- a/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift +++ b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift @@ -1,4 +1,5 @@ import Combine +import CombineSchedulers import ComposableArchitecture import XCTest diff --git a/Tests/ComposableArchitectureTests/ReducerTests.swift b/Tests/ComposableArchitectureTests/ReducerTests.swift index b48060fadaf9..70abf54f3f19 100644 --- a/Tests/ComposableArchitectureTests/ReducerTests.swift +++ b/Tests/ComposableArchitectureTests/ReducerTests.swift @@ -1,4 +1,5 @@ import Combine +import CombineSchedulers import ComposableArchitecture import XCTest import os.signpost diff --git a/Tests/ComposableArchitectureTests/SchedulerTests.swift b/Tests/ComposableArchitectureTests/SchedulerTests.swift deleted file mode 100644 index f90a61062eb4..000000000000 --- a/Tests/ComposableArchitectureTests/SchedulerTests.swift +++ /dev/null @@ -1,162 +0,0 @@ -import Combine -import ComposableArchitecture -import XCTest - -final class SchedulerTests: XCTestCase { - var cancellables: Set = [] - - func testAdvance() { - let scheduler = DispatchQueue.testScheduler - - var value: Int? - Just(1) - .delay(for: 1, scheduler: scheduler) - .sink { value = $0 } - .store(in: &self.cancellables) - - XCTAssertEqual(value, nil) - - scheduler.advance(by: .milliseconds(250)) - - XCTAssertEqual(value, nil) - - scheduler.advance(by: .milliseconds(250)) - - XCTAssertEqual(value, nil) - - scheduler.advance(by: .milliseconds(250)) - - XCTAssertEqual(value, nil) - - scheduler.advance(by: .milliseconds(250)) - - XCTAssertEqual(value, 1) - } - - func testRunScheduler() { - let scheduler = DispatchQueue.testScheduler - - var value: Int? - Just(1) - .delay(for: 1_000_000_000, scheduler: scheduler) - .sink { value = $0 } - .store(in: &self.cancellables) - - XCTAssertEqual(value, nil) - - scheduler.advance(by: 1_000_000) - - XCTAssertEqual(value, nil) - - scheduler.run() - - XCTAssertEqual(value, 1) - } - - func testDelay0Advance() { - let scheduler = DispatchQueue.testScheduler - - var value: Int? - Just(1) - .delay(for: 0, scheduler: scheduler) - .sink { value = $0 } - .store(in: &self.cancellables) - - XCTAssertEqual(value, nil) - - scheduler.advance() - - XCTAssertEqual(value, 1) - } - - func testSubscribeOnAdvance() { - let scheduler = DispatchQueue.testScheduler - - var value: Int? - Just(1) - .subscribe(on: scheduler) - .sink { value = $0 } - .store(in: &self.cancellables) - - XCTAssertEqual(value, nil) - - scheduler.advance() - - XCTAssertEqual(value, 1) - } - - func testReceiveOnAdvance() { - let scheduler = DispatchQueue.testScheduler - - var value: Int? - Just(1) - .receive(on: scheduler) - .sink { value = $0 } - .store(in: &self.cancellables) - - XCTAssertEqual(value, nil) - - scheduler.advance() - - XCTAssertEqual(value, 1) - } - - func testDispatchQueueDefaults() { - let scheduler = DispatchQueue.testScheduler - scheduler.advance(by: .nanoseconds(0)) - - XCTAssertEqual( - scheduler.now, - .init(DispatchTime(uptimeNanoseconds: 1)), - """ - Default of dispatchQueue.now should not be 0 because that has special meaning in DispatchTime's \ - initializer and causes it to default to DispatchTime.now(). - """ - ) - } - - func testTwoIntervalOrdering() { - let testScheduler = DispatchQueue.testScheduler - - var values: [Int] = [] - - testScheduler.schedule(after: testScheduler.now, interval: 2) { values.append(1) } - .store(in: &self.cancellables) - - testScheduler.schedule(after: testScheduler.now, interval: 1) { values.append(42) } - .store(in: &self.cancellables) - - XCTAssertEqual(values, []) - testScheduler.advance() - XCTAssertEqual(values, [1, 42]) - testScheduler.advance(by: 2) - XCTAssertEqual(values, [1, 42, 42, 1, 42]) - } - - func testDebounceReceiveOn() { - let scheduler = DispatchQueue.testScheduler - - let subject = PassthroughSubject() - - var count = 0 - subject - .debounce(for: 1, scheduler: scheduler) - .receive(on: scheduler) - .sink { count += 1 } - .store(in: &self.cancellables) - - XCTAssertEqual(count, 0) - - subject.send() - XCTAssertEqual(count, 0) - - scheduler.advance(by: 1) - XCTAssertEqual(count, 1) - - scheduler.advance(by: 1) - XCTAssertEqual(count, 1) - - scheduler.run() - XCTAssertEqual(count, 1) - } -}