Updated March 2020 with more thoroughly accurate timing diagrams, after vetting against iOS13.2, iOS 13.3, and iOS13.4 beta.
Combine was announced and released this past summer with iOS 13. And with this recent iOS 13 update, it is still definitely settling into place. While writing Using Combine, I wrote a number of tests to verify and generally double-check my understanding of how Combine was working. With the update to iOS 13.3, the tests showed me that a few behaviors changed once again.
The operator that changed and trigged my tests was throttle. Throttle is meant to act on values being received from an upstream publisher over a sliding window. It collapses the values flowing through a pipeline over time, choosing a representative value from the set that appeared within a given time window. It also turns out that throttle has slightly different behavior when you’re working with a publisher that starts out sending down an initial value (such as a
While I was poking at throttle to understand how it changed, I also realized that debounce was acting differently than I had originally understood, so I took some time to write some additional tests and make a more explicit timing diagram for it as well. Since debounce didn’t change behavior between 13.2 and 13.3 (that I spotted anyway), I’ll describe it first.
When you set up a debounce operator, you specify a time-window for it to react within. The operator collects all values that come in from the publisher, and most notably it resets the starting point for that sliding time window when it receives a value. It won’t send any values on until that entire window has expired without any other values appearing. In effect, it’s waiting for the value to settle. The marble diagrams show this really well.
debounce, you will see a delay invoked between when the event was published and the time it is received by your subscriber that is at least the value that you are processing the debounce across (0.5 seconds in the above example).
Throttle acts similarly to debounce, in that it collects multiple results over time and sends out a single result – but it does so with fixed time windows. Where debounce will reset the start of that window, throttle does not – so it doesn’t collapse the values entirely, but sort of “slows them down” (and that matches the name of the operator pretty well).
Which value from the set that arrive that’s chosen to be propagated is influenced by the parameter
latest, which is set when you create the operator. In general, latest being set to
true results in the last value appearing getting chosen, and latest being set to
false results in the first value that appears. This is one of those items that is a lot easier to understand by looking at a marble diagram:
The notable behavior change is how it handles initial values. Initial value seems to be a bit “flexible” in what is specifically initial though. When I first wrote my tests, I was using a class with a
@Published variable, which sends a value upon subscription, and then updates when it is changed. To illustrate the 13.3 behavior change better, I re-wrote and expanded those tests to use a PassthroughSubject, so there wasn’t an automatic initial value.
So under iOS 13.3, the initial value (which is sent out roughly 100ms after the creation of the pipeline in my test), is always propagated, and then the sliding window effect begins immediately after that.
throttle, you will see almost no delay between when the event was published and the time it is received by your subscriber. In the tests where I work the timing, using throttle with
latest=trueshows almost no delay, and throttle with
latest=falsewith a very short delay: 20 to 40 ms in my test environment.
debounce, by comparison, will always include a significant delay.
If you want to see the underlying tests that illustrate this, check out the following bits of code from the Using Combine project: