Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds the interval operator. #810

Merged
merged 5 commits into from
Jan 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<U, E> { (observer: Signal<U, E>.Observer, lifetime: Lifetime) in
lifetime += trickOrTreat.observe(observer)
}

// Obsolete: Returning a `Disposable`.
let candies = Signal { (observer: Signal<U, E>.Observer) -> Disposable? in
return trickOrTreat.observe(observer)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -266,12 +267,12 @@ let producer = SignalProducer<Int, NoError> { 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)

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
64 changes: 64 additions & 0 deletions Sources/SignalProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<S: Sequence>(
_ values: S,
interval: DispatchTimeInterval,
on scheduler: DateScheduler
) -> SignalProducer<S.Element, Error> 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)
}
}
65 changes: 65 additions & 0 deletions Tests/ReactiveSwiftTests/SignalProducerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Character, Never>?
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<Bool>!
Expand Down