Skip to content

Adds the interval operator. - #810

Merged
andersio merged 5 commits into
ReactiveCocoa:masterfrom
mluisbrown:michael/interval
Jan 9, 2021
Merged

andersio merged 5 commits into
ReactiveCocoa:masterfrom
mluisbrown:michael/interval

Conversation

@mluisbrown

Copy link
Copy Markdown
Contributor

Adds the interval operator with functionality similar to that of the operator with the same name in ReactiveX.

Allows specifying any Sequence, including infinite sequences such as 0... (which is the sequence used if none is provided, to mimic the ReactiveX behaviour).

This can be useful, for example, to make a "typewriter" effect of characters appearing on the screen at regularly spaced intervals.

Checklist

  • Updated CHANGELOG.md.

@mluisbrown mluisbrown changed the title Added the interval operator. Adds the interval operator. Dec 21, 2020
@andersio

andersio commented Jan 1, 2021 •

Copy link
Copy Markdown
Member

In case you missed it, we have a more generalized collect(every:on:) operator with two customizable options. Could you share your use case if you still deemed it achievable only with interval()?

/// Forward the latest values on `scheduler` every `interval`.
///
/// - note: If `self` terminates while values are being accumulated,
/// the behaviour will be determined by `discardWhenCompleted`.
/// If `true`, the values will be discarded and the returned producer
/// will terminate immediately.
/// If `false`, that values will be delivered at the next interval.
///
/// - parameters:
/// - interval: A repetition interval.
/// - scheduler: A scheduler to send values on.
/// - skipEmpty: Whether empty arrays should be sent if no values were
/// accumulated during the interval.
/// - discardWhenCompleted: A boolean to indicate if the latest unsent
/// values should be discarded on completion.
///
/// - returns: A producer that sends all values that are sent from `self`
/// at `interval` seconds apart.
public func collect(every interval: DispatchTimeInterval, on scheduler: DateScheduler, skipEmpty: Bool = false, discardWhenCompleted: Bool = true) -> SignalProducer<[Value], Error> {

@mluisbrown

mluisbrown commented Jan 4, 2021 •

Copy link
Copy Markdown
Contributor Author

The collect(every:on:) operator is not quite the same thing, and I'm not sure it can be used to replicate what can be achieved using interval. For example:

SignalProducer("abcde")
    .collect(every: .milliseconds(50), on: QueueScheduler.main, discardWhenCompleted: false)
    .startWithValues {
        print("\(Date()): \($0)")
}

will print 2021-01-04 12:13:03 +0000: ["a", "b", "c", "d", "e"], ie all the values a collected and emitted at the same time.

Whereas:

SignalProducer.interval("abcde", interval: .milliseconds(50), on: QueueScheduler.main)
    .startWithValues {
        print("\(Date()): \($0)")
}

will print each value in the sequence at 50ms intervals. One use case for this is for animating the appearance of text in the UI, as in this example:

AnimatedTitle.mp4

@mluisbrown

Copy link
Copy Markdown
Contributor Author

The alternative to using interval for the use case above is something like this:

SignalProducer(title)
    .flatMap(.concat) { char in
        SignalProducer(value: char)
            .delay(0.05, on: QueueScheduler.main)
    }

@mluisbrown

Copy link
Copy Markdown
Contributor Author

Other than that, interval exists as an operator in other FRP libraries so it would be nice to have it here also.

@andersio

andersio commented Jan 4, 2021 •

Copy link
Copy Markdown
Member

@mluisbrown

Seems like I might have misunderstood what interval() does in Rx.

Judging by your description, it seems SignalProducer.Type.timer should satisfy your need, except that it emits the target date as the value, instead of repeating a predefined constant.

SignalProducer
    .timer(.milliseconds(5), on: QueueScheduler.main)
    .map { date in (date, "abcde") }

    // or
    .map(value: "abcde")

@mluisbrown

Copy link
Copy Markdown
Contributor Author

@andersio

it seems SignalProducer.Type.timer should satisfy your need

Wouldn't the example you provided just emit the value "abcde" every 50 milliseconds? What I'm trying to do is emit each element of the sequence at the defined interval, until it ends (or for ever, in the case of an infinite sequence).

time value
0.050 "a"
0.100 "b"
0.150 "c"
0.200 "d"
0.250 "e"

FWIW: I tried creating this using timer and was unable to, ending up with the solution using flatMap and delay until I created a more generic interval solution.

@andersio

andersio commented Jan 4, 2021 •

Copy link
Copy Markdown
Member

Argh, missed the part of emitting elements in the collection one by one. Somehow thought it was repeating the string literal regularly.

I think zip would help in this use case?

SignalProducer.zip(
    SignalProducer.timer(.milliseconds(5), on: QueueScheduler.main),
    SignalProducer("Hello world!") // `SignalProducer<Character, Never>
)
.scan(into: "") { $0.append($1.1) }

Should work alright for small arrays/sequences.

interval() would indeed be more efficient though for sequences that are large or must be lazily consumed.

@mluisbrown

mluisbrown commented Jan 4, 2021 •

Copy link
Copy Markdown
Contributor Author

The zip with timer and scan approach would work. So would the flatMap and delay approach I mentioned above. There are probably a few other approaches that would work.

The idea of adding the interval operator though is to have an efficient solution that can work for any sequence of any length, including lazy sequences.

Given there is prior art for this in Rx, what is the objection to adding it?

When I needed this functionality I was sure there would be an operator for it, and a bit surprised that there wasn't.


it("shouldn't overflow on a real scheduler") {
let scheduler = QueueScheduler.makeForTesting()
let producer = SignalProducer.interval("abc", interval: .seconds(3), on: scheduler)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe provide an astronomically large sequence with repeatElement?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@andersio

andersio commented Jan 4, 2021 •

Copy link
Copy Markdown
Member

@mluisbrown There is no objection, more about trying to establish the need/value other than RxSwift parity (which has never been a USP of RAS). :)

@mluisbrown

mluisbrown commented Jan 5, 2021 •

Copy link
Copy Markdown
Contributor Author

other than RxSwift parity (which has never been a USP of RAS). :)

😄 I just meant that the fact that it already exists in ReactiveX is one more factor in evaluating the usefulness.

FWIW interval in RxSwift is not really the same thing, and is basically just a version of timer.

The prior art I was referring to was from ReactiveX.io

@andersio
andersio merged commit d1c7b8d into ReactiveCocoa:master Jan 9, 2021
@RuiAAPeres

Copy link
Copy Markdown
Member

Nice. 🤠

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants