Skip to content

Commit d1c7b8d

Browse files
mluisbrownandersio
andauthored
Adds the interval operator. (#810)
* Added the `interval` operator. * Added CHANGELOG entry. * Added PR number to changelog entry. * Use a very large sequence in the test. Co-authored-by: Anders Ha <[email protected]>
1 parent 8562161 commit d1c7b8d

File tree

3 files changed

+148
-18
lines changed

3 files changed

+148
-18
lines changed

CHANGELOG.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# master
22
*Please add new entries at the top.*
33

4+
1. Added the `interval` operator (#810, kudos to @mluisbrown)
45
# 6.5.0
56

67
1. Add `ExpressibleByNilLiteral` constraint to `OptionalProtocol` (#805, kudos to @nkristek)
78

8-
1. Fixed a `SignalProducer.lift` issue which may leak intermediate signals. (#808)
9+
1. Fixed a `SignalProducer.lift` issue which may leak intermediate signals. (#808)
910

1011
1. Add variadic sugar for boolean static methods such as `Property.any(boolProperty1, boolProperty2, boolProperty3)` (#801, kudos to @fortmarek)
1112

@@ -103,11 +104,11 @@
103104

104105
# 4.0.0-rc.2
105106

106-
1. Support Swift 4.2 (Xcode 10) (#644, kudos to @ikesyo)
107+
1. Support Swift 4.2 (Xcode 10) (#644, kudos to @ikesyo)
107108

108109
# 4.0.0-rc.1
109110

110-
1. `Lifetime` may now be manually ended using `Lifetime.Token.dispose()`, in addition to the existing when-token-deinitializes semantic. (#641, kudos to @andersio)
111+
1. `Lifetime` may now be manually ended using `Lifetime.Token.dispose()`, in addition to the existing when-token-deinitializes semantic. (#641, kudos to @andersio)
111112
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)
112113
1. For Swift 4.1 and above, `Signal.Event` and `ActionError` are now conditionally `Equatable`. (#590, kudos to @NachoSoto and @andersio)
113114
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 @@
159160
1. `Signal` now uses `Lifetime` for resource management. (#404, kudos to @andersio)
160161

161162
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.
162-
163+
163164
```swift
164165
// New: Add `Disposable`s to the `Lifetime`.
165166
let candies = Signal<U, E> { (observer: Signal<U, E>.Observer, lifetime: Lifetime) in
166167
lifetime += trickOrTreat.observe(observer)
167168
}
168-
169+
169170
// Obsolete: Returning a `Disposable`.
170171
let candies = Signal { (observer: Signal<U, E>.Observer) -> Disposable? in
171172
return trickOrTreat.observe(observer)
@@ -226,7 +227,7 @@
226227
1. The performance of `SignalProducer` has been improved significantly. (#140, kudos to @andersio)
227228

228229
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.
229-
230+
230231
1. `interrupted` now respects `observe(on:)`. (#140)
231232

232233
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<Int, NoError> { observer, lifetime in
266267

267268
Two `Disposable`-accepting methods `Lifetime.Type.+=` and `Lifetime.add` are provided to aid migration, and are subject to removal in a future release.
268269

269-
### Signal and SignalProducer
270+
### Signal and SignalProducer
270271
1. All `Signal` and `SignalProducer` operators now belongs to the respective concrete types. (#304)
271272

272273
Custom operators should extend the concrete types directly. `SignalProtocol` and `SignalProducerProtocol` should be used only for constraining associated types.
273274

274-
1. `combineLatest` and `zip` are optimised to have a constant overhead regardless of arity, mitigating the possibility of stack overflow. (#345)
275+
1. `combineLatest` and `zip` are optimised to have a constant overhead regardless of arity, mitigating the possibility of stack overflow. (#345)
275276

276277
1. `flatMap(_:transform:)` is renamed to `flatMap(_:_:)`. (#339)
277278

@@ -328,7 +329,7 @@ Two `Disposable`-accepting methods `Lifetime.Type.+=` and `Lifetime.add` are pro
328329
`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.
329330

330331
1. New operators: `reduce(into:)` and `scan(into:)`. (#365, kudos to @ikesyo)
331-
332+
332333
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.
333334

334335
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
348349
## Deprecation
349350
1. `observe(_:during:)` is now deprecated. It would be removed in ReactiveSwift 2.0.
350351
Use `take(during:)` and the relevant observation API of `Signal`, `SignalProducer` and `Property` instead. (#374)
351-
352+
352353
# 1.1.2
353354
## Changes
354355
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
402403

403404
Major changes since ReactiveCocoa 4 include:
404405
- **Updated for Swift 3**
405-
406+
406407
APIs have been updated and renamed to adhere to the Swift 3 [API Design Guidelines](https://swift.org/documentation/api-design-guidelines/).
407408
- **Signal Lifetime Semantics**
408-
409+
409410
`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.
410411
- **Reactive Proxies**
411-
412+
412413
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`.
413414
- **Property Composition**
414-
415+
415416
`Property`s can now be composed. They expose many of the familiar operators from `Signal` and `SignalProducer`, including `map`, `flatMap`, `combineLatest`, etc.
416417
- **Binding Primitives**
417-
418+
418419
`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.
419420
- **Lifetime**
420-
421+
421422
`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.
422423
- **Race-free Action**
423-
424+
424425
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.
425-
426+
426427
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.
427428

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

Sources/SignalProducer.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3038,3 +3038,67 @@ extension SignalProducer where Value == Date, Error == Never {
30383038
}
30393039
}
30403040
}
3041+
3042+
extension SignalProducer where Error == Never {
3043+
/// Creates a producer that will send the values from the given sequence
3044+
/// separated by the given time interval.
3045+
///
3046+
/// - note: If `values` is an infinite sequeence this `SignalProducer` will never complete naturally,
3047+
/// so all invocations of `start()` must be disposed to avoid leaks.
3048+
///
3049+
/// - precondition: `interval` must be non-negative number.
3050+
///
3051+
/// - parameters:
3052+
/// - values: A sequence of values that will be sent as separate
3053+
/// `value` events and then complete.
3054+
/// - interval: An interval between value events.
3055+
/// - scheduler: A scheduler to deliver events on.
3056+
///
3057+
/// - returns: A producer that sends the next value from the sequence every `interval` seconds.
3058+
public static func interval<S: Sequence>(
3059+
_ values: S,
3060+
interval: DispatchTimeInterval,
3061+
on scheduler: DateScheduler
3062+
) -> SignalProducer<S.Element, Error> where S.Iterator.Element == Value {
3063+
3064+
return SignalProducer { observer, lifetime in
3065+
var iterator = values.makeIterator()
3066+
3067+
lifetime += scheduler.schedule(
3068+
after: scheduler.currentDate.addingTimeInterval(interval),
3069+
interval: interval,
3070+
// Apple's "Power Efficiency Guide for Mac Apps" recommends a leeway of
3071+
// at least 10% of the timer interval.
3072+
leeway: interval * 0.1,
3073+
action: {
3074+
switch iterator.next() {
3075+
case let .some(value):
3076+
observer.send(value: value)
3077+
case .none:
3078+
observer.sendCompleted()
3079+
}
3080+
}
3081+
)
3082+
}
3083+
}
3084+
3085+
/// Creates a producer that will send the sequence of all integers
3086+
/// from 0 to infinity, or until disposed.
3087+
///
3088+
/// - note: This timer will never complete naturally, so all invocations of
3089+
/// `start()` must be disposed to avoid leaks.
3090+
///
3091+
/// - precondition: `interval` must be non-negative number.
3092+
///
3093+
/// - parameters:
3094+
/// - interval: An interval between value events.
3095+
/// - scheduler: A scheduler to deliver events on.
3096+
///
3097+
/// - returns: A producer that sends a sequential `Int` value every `interval` seconds.
3098+
public static func interval(
3099+
_ interval: DispatchTimeInterval,
3100+
on scheduler: DateScheduler
3101+
) -> SignalProducer where Value == Int {
3102+
.interval(0..., interval: interval, on: scheduler)
3103+
}
3104+
}

Tests/ReactiveSwiftTests/SignalProducerSpec.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,71 @@ class SignalProducerSpec: QuickSpec {
12571257
}
12581258
}
12591259

1260+
describe("interval") {
1261+
it("should send the next sequence value at the given interval") {
1262+
let scheduler = TestScheduler()
1263+
let producer = SignalProducer.interval("abc", interval: .seconds(1), on: scheduler)
1264+
1265+
var isDisposed = false
1266+
var values: [Character] = []
1267+
producer
1268+
.on(disposed: { isDisposed = true })
1269+
.startWithValues { values.append($0) }
1270+
1271+
scheduler.advance(by: .milliseconds(900))
1272+
expect(values) == []
1273+
1274+
scheduler.advance(by: .seconds(1))
1275+
expect(values) == ["a"]
1276+
1277+
scheduler.advance()
1278+
expect(values) == ["a"]
1279+
1280+
scheduler.advance(by: .milliseconds(200))
1281+
expect(values) == ["a", "b"]
1282+
1283+
scheduler.advance(by: .seconds(1))
1284+
expect(values) == ["a", "b", "c"]
1285+
1286+
scheduler.advance(by: .seconds(1))
1287+
expect(isDisposed) == true
1288+
}
1289+
1290+
it("shouldn't overflow on a real scheduler") {
1291+
let scheduler = QueueScheduler.makeForTesting()
1292+
let testSequence = repeatElement(Character("a"), count: 1_000_000)
1293+
let producer = SignalProducer.interval(testSequence, interval: .seconds(3), on: scheduler)
1294+
producer
1295+
.start()
1296+
.dispose()
1297+
}
1298+
1299+
it("should dispose of the signal when disposed") {
1300+
let scheduler = TestScheduler()
1301+
let producer = SignalProducer.interval("abc", interval: .seconds(1), on: scheduler)
1302+
var interrupted = false
1303+
1304+
var isDisposed = false
1305+
weak var weakSignal: Signal<Character, Never>?
1306+
producer.startWithSignal { signal, disposable in
1307+
weakSignal = signal
1308+
scheduler.schedule {
1309+
disposable.dispose()
1310+
}
1311+
signal.on(disposed: { isDisposed = true }).observeInterrupted { interrupted = true }
1312+
}
1313+
1314+
expect(weakSignal).to(beNil())
1315+
expect(isDisposed) == false
1316+
expect(interrupted) == false
1317+
1318+
scheduler.run()
1319+
expect(weakSignal).to(beNil())
1320+
expect(isDisposed) == true
1321+
expect(interrupted) == true
1322+
}
1323+
}
1324+
12601325
describe("throttle while") {
12611326
var scheduler: ImmediateScheduler!
12621327
var shouldThrottle: MutableProperty<Bool>!

0 commit comments

Comments
 (0)