Reactive Programming in iOS with Combine

Feb 4 2021 · Swift 5.3, macOS 11.0, Xcode 12.2

Part 4: Timing, Scheduling and Sequencing Operators

23. Delay and Collect

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 22. Introduction Next episode: 24. Debounce and Throttle

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 23. Delay and Collect

Have you ever heard the saying “Timing is everything”? That is especially true in Combine, which models asynchronous event flows over time. It’s no surprise then that Combine has a set of operators that deal with time, in particular how sequences react to and transform values over time. In this episode you’ll learn about two timing operators: delay and collect.

The most basic time manipulation operator is delay. This operator delays the values emitted by a publisher so that you see them later than they actually occur. The delay(for:tolerance:scheduler:option) operator time shifts a whole sequence of values. Each time the publisher emits a value, delay keeps a hold of it for the specified duration, then emits it downstream. This happens on the Scheduler you specify. Let’s take a look at this in a demo.

To better visualize what is going on with timing operators, you’ll use an animated Xcode Playground that visualizes how data flows over time. In the Starter playground for this project, you’ll find that it is divided into several pages. Each page will exercise one or more operators in this episode. The playground also includes some ready-made classes, functions and sample data that will come in handy.

Enable Show Rendered Markup in the playground if you aren’t already and you’ll see a Next link that will take you to the next page. To get the most benefit from this playground, make sure that the Live View is enabled by using the buttons in the upper right hand corner of the editor.

Also remember that playgrounds can be run automatically or manually. If you choose Manually Run here, you can use the play button to run the playground. If by chance the playground doesn’t run properly, you can go to Preferences -> Locations, and click the arrow next to the Derived Data location. This will show the derived data folder in finder. Quit Xcode, move the derived data folder to the trash, and then restart Xcode. Your playground should now work properly.

Now, with that delay, let’s talk about…. delay! Define a few constant to use:

let valuesPerSecond = 1.0
let delayInSeconds = 1.5

Create a publisher that emits a value every second and delays it by 1.5 seconds. Display both timelines simultaneously to compare them. To accomplish this, add a PassthroughSubject which will take in dates emitted by a Timer and call it sourcePublisher.

// 1
let sourcePublisher = PassthroughSubject<Date, Never>()

Next make the delayedPublisher, which delays values emitted by the sourcePublisher, and emits them on the main scheduler.

// 2
let delayedPublisher = sourcePublisher.delay(for: .seconds(delayInSeconds), scheduler: DispatchQueue.main)

Create a timer that delivers one value per second on the main thread. Start it immediately with a call to autoconnect() and feed the values to the sourcePublisher.

// 3
let subscription = Timer
  .publish(every: 1.0 / elementsPerSecond, on: .main, in: .common)
  .autoconnect()
  .subscribe(sourcePublisher)

Now create the two views you will display in the playground, using the provided TimelineView view.

// 4
let sourceTimeline = TimelineView(title: "Emitted values (\(elementsPerSecond) per sec.):")

// 5
let delayedTimeline = TimelineView(title: "Delayed values (with a \(delayInSeconds)s delay):")

Finally, create a VStack to hold both timelines, and setup the liveView for this playground

// 6
let view = VStack(spacing: 50) {
  sourceTimeline
  delayedTimeline
}

// 7
PlaygroundPage.current.liveView = UIHostingController(rootView: view)

Run the playground. The timelines are empty! Feed them values emitted by each publisher. Add this code to the playground.

sourcePublisher.displayEvents(in: sourceTimeline)
delayedPublisher.displayEvents(in: delayedTimeline)

Run the playground again. You can clearly see the shift in the delayed values relative to the original emission times.

There may be cases where you want to collect a series of values over a period of time before performing an operation. This is where the collect(byTime) operator comes into play. Go to the collect page of the playground, and let’s look at that operator in a demo.

Start by defining some constants for the number of values per second, and the collect time.

let valuesPerSecond = 1.0
let collectTimeStride = 4

Make 2 publishers. The first is a PassthroughSubject that will emits values published by a timer. The second publisher collects values from the sourcePublisher, and emits those values on the main queue scheduler, collecting values over a stride defined by collectTimeStride.

// 1
let sourcePublisher = PassthroughSubject<Date, Never>()

// 2
let collectedPublisher = sourcePublisher
  .collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))

Use a Timer to emit values at regular intervals, on the main scheduler, using autoconnect to start is right away, and send the events to the sourcePublisher.

let subscription = Timer
  .publish(every: 1.0 / valuesPerSecond, on: .main, in: .common)
  .autoconnect()
  .subscribe(sourcePublisher)

Make the timeline views like the delay example so you can see the emitted values and the collected values side by side. Place the views in a VStack to display them on screen, and set the liveView for the playground.

let sourceTimeline = TimelineView(title: "Emitted values:")
let collectedTimeline = TimelineView(title: "Collected values (every \(collectTimeStride)s):")

let view = VStack(spacing: 40) {
  sourceTimeline
  collectedTimeline
}

PlaygroundPage.current.liveView = UIHostingController(rootView: view)

Finally, feed the timeline views events from the publishers.

sourcePublisher.displayEvents(in: sourceTimeline)
collectedPublisher.displayEvents(in: collectedTimeline)

Run the playground. The sourcePublisher emits every second, while the collected value shows an emission every 4 seconds. But what exactly is emitted? Go back and update the collectedPublisher to include a flatMap operation.

let collectedPublisher = sourcePublisher
  .collect(.byTime(DispatchQueue.main, .seconds(collectTimeStride)))
  .flatMap { dates in dates.publisher }

Every time collect emits a group of values it collected, flatMap breaks it down again to individual values but emitted all at the same time. To this end, it uses the publisher extension of Collection that turns a sequence of values into a Publisher, emitting immediately all values in the sequence as individual values. Rerun the playground and now you can see all the collected values.

In this episode you learned about two timing operators: delay delays the values emitted by a publisher so that you see them later than they actually occur, and collect collects a series of values over a period of time before performing an operation, which is useful when performing operations like averages.

In the next episode you’ll learn about 2 other timing operators: debounce and throttle