From 438c2bb27b08e4c4a05fa0723c2d5cd5c645c0d6 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Mon, 21 Dec 2020 14:39:07 +0000 Subject: [PATCH 1/4] Added the `interval` operator. --- Sources/SignalProducer.swift | 64 +++++++++++++++++++ .../SignalProducerSpec.swift | 64 +++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/Sources/SignalProducer.swift b/Sources/SignalProducer.swift index 4d67cee70..c488a2599 100644 --- a/Sources/SignalProducer.swift +++ b/Sources/SignalProducer.swift @@ -3038,3 +3038,67 @@ extension SignalProducer where Value == Date, Error == Never { } } } + +extension SignalProducer where Error == Never { + /// Creates a producer that will send the values from the given sequence + /// separated by the given time interval. + /// + /// - note: If `values` is an infinite sequeence this `SignalProducer` will never complete naturally, + /// so all invocations of `start()` must be disposed to avoid leaks. + /// + /// - precondition: `interval` must be non-negative number. + /// + /// - parameters: + /// - values: A sequence of values that will be sent as separate + /// `value` events and then complete. + /// - interval: An interval between value events. + /// - scheduler: A scheduler to deliver events on. + /// + /// - returns: A producer that sends the next value from the sequence every `interval` seconds. + public static func interval( + _ values: S, + interval: DispatchTimeInterval, + on scheduler: DateScheduler + ) -> SignalProducer where S.Iterator.Element == Value { + + return SignalProducer { observer, lifetime in + var iterator = values.makeIterator() + + lifetime += scheduler.schedule( + after: scheduler.currentDate.addingTimeInterval(interval), + interval: interval, + // Apple's "Power Efficiency Guide for Mac Apps" recommends a leeway of + // at least 10% of the timer interval. + leeway: interval * 0.1, + action: { + switch iterator.next() { + case let .some(value): + observer.send(value: value) + case .none: + observer.sendCompleted() + } + } + ) + } + } + + /// Creates a producer that will send the sequence of all integers + /// from 0 to infinity, or until disposed. + /// + /// - note: This timer will never complete naturally, so all invocations of + /// `start()` must be disposed to avoid leaks. + /// + /// - precondition: `interval` must be non-negative number. + /// + /// - parameters: + /// - interval: An interval between value events. + /// - scheduler: A scheduler to deliver events on. + /// + /// - returns: A producer that sends a sequential `Int` value every `interval` seconds. + public static func interval( + _ interval: DispatchTimeInterval, + on scheduler: DateScheduler + ) -> SignalProducer where Value == Int { + .interval(0..., interval: interval, on: scheduler) + } +} diff --git a/Tests/ReactiveSwiftTests/SignalProducerSpec.swift b/Tests/ReactiveSwiftTests/SignalProducerSpec.swift index 4de6fbe29..3631d9e3d 100644 --- a/Tests/ReactiveSwiftTests/SignalProducerSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalProducerSpec.swift @@ -1257,6 +1257,70 @@ class SignalProducerSpec: QuickSpec { } } + describe("interval") { + it("should send the next sequence value at the given interval") { + let scheduler = TestScheduler() + let producer = SignalProducer.interval("abc", interval: .seconds(1), on: scheduler) + + var isDisposed = false + var values: [Character] = [] + producer + .on(disposed: { isDisposed = true }) + .startWithValues { values.append($0) } + + scheduler.advance(by: .milliseconds(900)) + expect(values) == [] + + scheduler.advance(by: .seconds(1)) + expect(values) == ["a"] + + scheduler.advance() + expect(values) == ["a"] + + scheduler.advance(by: .milliseconds(200)) + expect(values) == ["a", "b"] + + scheduler.advance(by: .seconds(1)) + expect(values) == ["a", "b", "c"] + + scheduler.advance(by: .seconds(1)) + expect(isDisposed) == true + } + + it("shouldn't overflow on a real scheduler") { + let scheduler = QueueScheduler.makeForTesting() + let producer = SignalProducer.interval("abc", interval: .seconds(3), on: scheduler) + producer + .start() + .dispose() + } + + it("should dispose of the signal when disposed") { + let scheduler = TestScheduler() + let producer = SignalProducer.interval("abc", interval: .seconds(1), on: scheduler) + var interrupted = false + + var isDisposed = false + weak var weakSignal: Signal? + producer.startWithSignal { signal, disposable in + weakSignal = signal + scheduler.schedule { + disposable.dispose() + } + signal.on(disposed: { isDisposed = true }).observeInterrupted { interrupted = true } + } + + expect(weakSignal).to(beNil()) + expect(isDisposed) == false + expect(interrupted) == false + + scheduler.run() + expect(weakSignal).to(beNil()) + expect(isDisposed) == true + expect(interrupted) == true + } + } + describe("throttle while") { var scheduler: ImmediateScheduler! var shouldThrottle: MutableProperty! From 669ba35ff390cece387bba252b6f8d801581c913 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Mon, 21 Dec 2020 14:42:34 +0000 Subject: [PATCH 2/4] Added CHANGELOG entry. --- CHANGELOG.md | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4cba75f2..9f2b5f8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # master *Please add new entries at the top.* +1. Added the `interval` operator (kudos to @mluisbrown) # 6.5.0 1. Add `ExpressibleByNilLiteral` constraint to `OptionalProtocol` (#805, kudos to @nkristek) -1. Fixed a `SignalProducer.lift` issue which may leak intermediate signals. (#808) +1. Fixed a `SignalProducer.lift` issue which may leak intermediate signals. (#808) 1. Add variadic sugar for boolean static methods such as `Property.any(boolProperty1, boolProperty2, boolProperty3)` (#801, kudos to @fortmarek) @@ -103,11 +104,11 @@ # 4.0.0-rc.2 -1. Support Swift 4.2 (Xcode 10) (#644, kudos to @ikesyo) +1. Support Swift 4.2 (Xcode 10) (#644, kudos to @ikesyo) # 4.0.0-rc.1 -1. `Lifetime` may now be manually ended using `Lifetime.Token.dispose()`, in addition to the existing when-token-deinitializes semantic. (#641, kudos to @andersio) +1. `Lifetime` may now be manually ended using `Lifetime.Token.dispose()`, in addition to the existing when-token-deinitializes semantic. (#641, kudos to @andersio) 1. For Swift 4.1 and above, `BindingSource` conformances are required to have `Error` parameterized as exactly `NoError`. As a result, `Signal` and `SignalProducer` are now conditionally `BindingSource`. (#590, kudos to @NachoSoto and @andersio) 1. For Swift 4.1 and above, `Signal.Event` and `ActionError` are now conditionally `Equatable`. (#590, kudos to @NachoSoto and @andersio) 1. New method `collect(every:on:skipEmpty:discardWhenCompleted:)` which delivers all values that occurred during a time interval (#619, kudos to @Qata) @@ -159,13 +160,13 @@ 1. `Signal` now uses `Lifetime` for resource management. (#404, kudos to @andersio) The `Signal` initialzer now accepts a generator closure that is passed with the input `Observer` and the `Lifetime` as its arguments. The original variant accepting a single-argument generator closure is now obselete. This is a source breaking change. - + ```swift // New: Add `Disposable`s to the `Lifetime`. let candies = Signal { (observer: Signal.Observer, lifetime: Lifetime) in lifetime += trickOrTreat.observe(observer) } - + // Obsolete: Returning a `Disposable`. let candies = Signal { (observer: Signal.Observer) -> Disposable? in return trickOrTreat.observe(observer) @@ -226,7 +227,7 @@ 1. The performance of `SignalProducer` has been improved significantly. (#140, kudos to @andersio) All lifted `SignalProducer` operators no longer yield an extra `Signal`. As a result, the calling overhead of event delivery is generally reduced proportionally to the level of chaining of lifted operators. - + 1. `interrupted` now respects `observe(on:)`. (#140) When a produced `Signal` is interrupted, if `observe(on:)` is the last applied operator, `interrupted` would now be delivered on the `Scheduler` passed to `observe(on:)` just like other events. @@ -266,12 +267,12 @@ let producer = SignalProducer { observer, lifetime in Two `Disposable`-accepting methods `Lifetime.Type.+=` and `Lifetime.add` are provided to aid migration, and are subject to removal in a future release. -### Signal and SignalProducer +### Signal and SignalProducer 1. All `Signal` and `SignalProducer` operators now belongs to the respective concrete types. (#304) Custom operators should extend the concrete types directly. `SignalProtocol` and `SignalProducerProtocol` should be used only for constraining associated types. -1. `combineLatest` and `zip` are optimised to have a constant overhead regardless of arity, mitigating the possibility of stack overflow. (#345) +1. `combineLatest` and `zip` are optimised to have a constant overhead regardless of arity, mitigating the possibility of stack overflow. (#345) 1. `flatMap(_:transform:)` is renamed to `flatMap(_:_:)`. (#339) @@ -328,7 +329,7 @@ Two `Disposable`-accepting methods `Lifetime.Type.+=` and `Lifetime.add` are pro `concurrent` starts and flattens inner signals according to the specified concurrency limit. If an inner signal is received after the limit is reached, it would be queued and drained later as the in-flight inner signals terminate. 1. New operators: `reduce(into:)` and `scan(into:)`. (#365, kudos to @ikesyo) - + These variants pass to the closure an `inout` reference to the accumulator, which helps the performance when a large value type is used, e.g. collection. 1. `Property(initial:then:)` gains overloads that accept a producer or signal of the wrapped value type when the value type is an `Optional`. (#396) @@ -348,7 +349,7 @@ Thank you to all of @ReactiveCocoa/reactiveswift and all our contributors, but e ## Deprecation 1. `observe(_:during:)` is now deprecated. It would be removed in ReactiveSwift 2.0. Use `take(during:)` and the relevant observation API of `Signal`, `SignalProducer` and `Property` instead. (#374) - + # 1.1.2 ## Changes 1. Fixed a rare occurrence of `interrupted` events being emitted by a `Property`. (#362) @@ -402,27 +403,27 @@ This is the first major release of ReactiveSwift, a multi-platform, pure-Swift f Major changes since ReactiveCocoa 4 include: - **Updated for Swift 3** - + APIs have been updated and renamed to adhere to the Swift 3 [API Design Guidelines](https://swift.org/documentation/api-design-guidelines/). - **Signal Lifetime Semantics** - + `Signal`s now live and continue to emit events only while either (a) they have observers or (b) they are retained. This clears up a number of unexpected cases and makes Signals much less dangerous. - **Reactive Proxies** - + Types can now declare conformance to `ReactiveExtensionsProvider` to expose a `reactive` property that’s generic over `self`. This property hosts reactive extensions to the type, such as the ones provided on `NotificationCenter` and `URLSession`. - **Property Composition** - + `Property`s can now be composed. They expose many of the familiar operators from `Signal` and `SignalProducer`, including `map`, `flatMap`, `combineLatest`, etc. - **Binding Primitives** - + `BindingTargetProtocol` and `BindingSourceProtocol` have been introduced to allow binding of observable instances to targets. `BindingTarget` is a new concrete type that can be used to wrap a settable but non-observable property. - **Lifetime** - + `Lifetime` is introduced to represent the lifetime of any arbitrary reference type. This can be used with the new `take(during:)` operator, but also forms part of the new binding APIs. - **Race-free Action** - + A new `Action` initializer `Action(state:enabledIf:_:)` has been introduced. It allows the latest value of any arbitrary property to be supplied to the execution closure in addition to the input from `apply(_:)`, while having the availability being derived from the property. - + This eliminates a data race in ReactiveCocoa 4.x, when both the `enabledIf` predicate and the execution closure depend on an overlapping set of properties. Extensive use of Swift’s `@available` declaration has been used to ease migration from ReactiveCocoa 4. Xcode should have fix-its for almost all changes from older APIs. From 7d0866f56824252eb72a4fe912458fccfb96f6d7 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Mon, 21 Dec 2020 14:48:36 +0000 Subject: [PATCH 3/4] Added PR number to changelog entry. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2b5f8d7..eea11b889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # master *Please add new entries at the top.* -1. Added the `interval` operator (kudos to @mluisbrown) +1. Added the `interval` operator (#810, kudos to @mluisbrown) # 6.5.0 1. Add `ExpressibleByNilLiteral` constraint to `OptionalProtocol` (#805, kudos to @nkristek) From f84c1ff92651f06e66e197ad1052d2f0c1acdd06 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Tue, 5 Jan 2021 10:15:26 +0000 Subject: [PATCH 4/4] Use a very large sequence in the test. --- Tests/ReactiveSwiftTests/SignalProducerSpec.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/ReactiveSwiftTests/SignalProducerSpec.swift b/Tests/ReactiveSwiftTests/SignalProducerSpec.swift index 3631d9e3d..56a5b4cfc 100644 --- a/Tests/ReactiveSwiftTests/SignalProducerSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalProducerSpec.swift @@ -1289,7 +1289,8 @@ class SignalProducerSpec: QuickSpec { it("shouldn't overflow on a real scheduler") { let scheduler = QueueScheduler.makeForTesting() - let producer = SignalProducer.interval("abc", interval: .seconds(3), on: scheduler) + let testSequence = repeatElement(Character("a"), count: 1_000_000) + let producer = SignalProducer.interval(testSequence, interval: .seconds(3), on: scheduler) producer .start() .dispose()