diff --git a/CHANGELOG.md b/CHANGELOG.md index a4cba75f2..eea11b889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # master *Please add new entries at the top.* +1. Added the `interval` operator (#810, 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. diff --git a/Sources/SignalProducer.swift b/Sources/SignalProducer.swift index b3cf87c9f..2ddcc19da 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 778e183d9..7335c9f19 100644 --- a/Tests/ReactiveSwiftTests/SignalProducerSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalProducerSpec.swift @@ -1257,6 +1257,71 @@ 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 testSequence = repeatElement(Character("a"), count: 1_000_000) + let producer = SignalProducer.interval(testSequence, 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!