Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

6. Time Manipulation Operators
Written by Florent Pillet

Timing is everything. The core idea behind reactive programming is to model asynchronous event flow over time. In this respect, the Combine framework provides a range of operators that allow you to deal with time. In particular, how sequences react to and transform values over time.

As you’ll see throughout this chapter, managing the time dimension of your sequence of values is easy and straightforward. It’s one of the great benefits of using a framework like Combine.

Getting started

To learn about time manipulation operators, you’ll practice with an animated Xcode Playground that visualizes how data flows over time. This chapter comes with a starter playground you’ll find in the projects folder.

The playground is divided into several pages. You’ll use each page to exercise one or more related operators. It also includes some ready-made classes, functions and sample data that’ll come in handy to build the examples.

If you have the playground set to show rendered markup, at the bottom of each page there will be a Next link that you can click to go to the next page.

Note: To toggle showing rendered markup on and off, select Editor ▸ Show Rendered/Raw Markup from the menu.

You can also select the page you want from the Project navigator in the left sidebar or even the jump bar at the top of the page. There are lots of ways to get around in Xcode!

Look at Xcode, and you’ll see the sidebar control at the top-left part of the window:

  1. Make sure the left sidebar button is toggled so you can see the list of Playground pages:

  1. Next, look at the top-right side of the window. You’ll see the view controls:

Show the editor with Live View. This will display a live view of the sequences you build in code. This is where the real action will happen!

  1. Showing the Debug area is important for most of the examples in this chapter. Toggle the Debug area using the following icon at the bottom-right of the window, or using the keyboard shortcut Command-Shift-Y:

Playground not working?

From time to time Xcode may “act up” and not run properly your playground. If this happens to you, open the Preferences dialog in Xcode and select the Locations tab. Click the arrow next to the Derived Data location, depicted by the red circled 1 in the screenshot below. It shows the DerivedData folder in the Finder.

Quit Xcode, move the DerivedData folder to trash then launch Xcode again. Your playground should now work properly!

Shifting time

Every now and again you need time traveling. While Combine can’t help with fixing your past relationship mistakes, it can freeze time for a little while to let you wait until self-cloning is available.

The most basic time manipulation operator delays values from a publisher so that you see them later than they actually occur.

The delay(for:tolerance:scheduler:options) operator time-shifts a whole sequence of values: Every time the upstream publisher emits a value, delay keeps it for a while then emits it after the delay you asked for, on the scheduler you specified.

Open the Delay playground page. The first thing you’ll see is that you’re not only importing the Combine framework but also SwiftUI! This animated playground is built with SwiftUI and Combine. When you feel in an adventurous mood, it’ll be a good idea to peruse through the code in the Sources folder.

But first things first. Start by defining a couple of constants you’ll be able to tweak later:

let valuesPerSecond = 1.0
let delayInSeconds = 1.5

You’re going to create a publisher that emits one value every second, then delay it by 1.5 seconds and display both timelines simultaneously to compare them. Once you complete the code on this page, you’ll be able to adjust the constants and watch results in the timelines.

Next, create the publishers you need:

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

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

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

Breaking this code down:

  1. sourcePublisher is a simple Subject which you’ll feed dates a Timer emits. The type of values is of little importance here. You only care about imaging when a publisher emits a value, and when the value shows up after a delay.

  2. delayedPublisher will delay values from a sourcePublisher and emit them on the main scheduler. You’ll learn all about schedulers in Chapter 17, “Schedulers.” For now, specify that values must end up on the main queue, ready for display to consume them.

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

Note: This particular timer is a Combine extension on the Foundation Timer class. It takes a RunLoop and RunLoop.Mode, and not a DispatchQueue as you may expect. You’ll learn all about timers in Chapter 11, “Timers.” Also, timers are part of a class of publishers that are connectable. This means they need to be connected to before they start emitting values. You use autoconnect() which immediately connects upon the first subscription.

You’re getting to the part where you create the two views that will let you visualize events. Add this code to your playground:

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

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

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

// 7
PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))

In this code, you:

  1. Create a TimelineView that will display values from the timer. TimelineView is a SwiftUI view, its code can be found at Sources/Views.swift.

  2. Create another TimelineView to display delayed values.

  3. Create a simple SwiftUI vertical stack to display both timelines one above the other.

  4. Set up the live view for this playground page. The additional frame(widht:height:) modifier is just there to help set a fixed frame for Xcode‘s previews.

At this stage, you see two empty timelines on the screen. You now need to feed them with the values from each publisher! Add this final code to the playground:

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

In this last piece of code, you connect the source and delayed publishers to their respective timelines to display events.

Once you save these source changes, Xcode will recompile the playground code and… look at the Live View pane! Finally!

You’ll see two timelines. The top timeline shows values the timer emitted. The bottom timeline shows the same values, delayed. The numbers inside the circles reflect the count of values emitted, not their actual content.

Note: As exciting as it is to see a live observable diagram, it might confuse at first. Static timelines usually have their values aligned to the left. But, if you think twice about it, they also have the most recent ones on the right side just as the animated diagrams you observe right now.

Collecting values

In certain situations, you may need to collect values from a publisher at specified time intervals. This is a form of buffering that can be useful. For example, when you want to average a group of values over short periods of time and output the average.

Switch to the Collect page by clicking the Next link at the bottom, or by selecting it in the Project navigator or jump bar.

As in the previous example, you’ll begin with some constants:

let valuesPerSecond = 1.0
let collectTimeStride = 4

Of course, reading these constants gives you an idea of where this is all going. Create your publishers now:

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

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

Like in the previous example, you:

  1. Set up a source publisher — a subject that will relay the values a timer emits.

  2. Create a collectedPublisher which collects values it receives during strides of collectTimeStride using the collect operator. The operator emits these groups of values as arrays on the specified scheduler: DispatchQueue.main.

Note: You might remember learning about the collect operator in Chapter 3, “Transforming Operators,” where you used a simple number to define how to group values together. This overload of collect accepts a strategy for grouping values; in this case, by time.

You’ll use a Timer again to emit values at regular intervals as you did for the delay operator:

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

Next, create the timeline views like in the previous example. Then, set the playground’s live view to a vertical stack showing the source timeline and the timeline of collected values:

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.frame(width: 375, height: 600))

Finally, feed the timelines with events from both publishers:

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

You’re done! Now look at the live view for a while:

You see values appear at regular intervals on the Emitted values timeline. Below it, you see that every four seconds the Collected values timeline displays a single value. But what is it?

You may have guessed that the value is an array of values received during the last four seconds. You can improve the display to see what’s actually in it! Go back to the line where you created the collectedPublisher object. Add the use of the flatMap operator just below it, so it looks like this:

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

Do you remember your friend flatMap you learned about in Chapter 3, “Transforming Operators?” You’re putting it to good use here: Every time collect emits a group of values it collected, flatMap breaks it down again to individual values but emitted immediately one after the other. 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.

Now, look at the effect it has on the timeline:

You can now see that, every four seconds, collect emits an array of values collected during the last time interval.

Collecting values (part 2)

The second option the collect(_:options:) operator offers allows you to keep collecting values at regular intervals. It also allows you to limit the number of values collected.

Staying on the same Collect page, and add a new constant right below collectTimeStride at the top:

let collectMaxCount = 2

Next, create a new publisher after collectedPublisher:

let collectedPublisher2 = sourcePublisher
  .collect(.byTimeOrCount(DispatchQueue.main,
                          .seconds(collectTimeStride),
                          collectMaxCount))
  .flatMap { dates in dates.publisher }

This time, you are using the .byTimeOrCount(Context, Context.SchedulerTimeType.Stride, Int) variant to collect up to collectMaxCount values at a time. What does this mean? Keep adding code and you’ll find out!

Add a new TimelineView for the second collect publisher in between collectedTimeline and let view = VStack...:

let collectedTimeline2 = TimelineView(title: "Collected values (at most \(collectMaxCount) every \(collectTimeStride)s):")

And of course add it to the list of stacked views, so view looks like this:

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

Finally, make sure it displays the events it emits in the timeline by adding the following at the end of your playground:

collectedPublisher2.displayEvents(in: collectedTimeline2)

Now, let this timeline run for a while so you can witness the difference:

You can see here that the second timeline is limiting its collection to two values at a time, as required by the collectMaxCount constant. It’s a useful tool to know about!

Holding off on events

When coding user interfaces, you frequently deal with text fields. Wiring up text field contents to an action using Combine is a common task. For example, you may want to send a search URL request that returns a list of items matching what’s typed in the text field.

But of course, you don’t want to send a request every time your user types a single letter! You need some kind of mechanism to help pick up on typed text only when the user is done typing for a while.

Combine offers two operators that can help you here: debounce and throttle. Let’s explore them!

Debounce

Switch to the playground page named Debounce. Make sure that the Debug area is visible — View ▸ Debug Area ▸ Activate Console — so you can see the printouts of values debounce emits.

Start by creating a couple of publishers:

// 1
let subject = PassthroughSubject<String, Never>()

// 2
let debounced = subject
  .debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
  // 3
  .share()

In this code, you:

  1. Create a source publisher which will emit strings.
  2. Use debounce to wait for one second on emissions from subject. Then, it will send the last value sent in that one-second interval, if any. This has the effect of allowing a max of one value per second to be sent.
  3. You are going to subscribe multiple times to debounced. To guarantee consistency of the results, you use share() to create a single subscription point to debounce that will show the same results at the same time to all subscribers.

Note: Diving into the share() operator is out of the scope of this chapter. Just remember that it is helpful when a single subscription to a publisher is required to deliver the same results to multiple subscribers. You’ll learn more about share() in Chapter 13, “Resource Management.”

For these next few examples, you will use a set of data to simulate a user typing text in a text field. Don’t type this in — it’s already been implemented in Sources/Data.swift for you:

public let typingHelloWorld: [(TimeInterval, String)] = [
  (0.0, "H"),
  (0.1, "He"),
  (0.2, "Hel"),
  (0.3, "Hell"),
  (0.5, "Hello"),
  (0.6, "Hello "),
  (2.0, "Hello W"),
  (2.1, "Hello Wo"),
  (2.2, "Hello Wor"),
  (2.4, "Hello Worl"),
  (2.5, "Hello World")
]

The simulated user starts typing at 0.0 seconds, pauses after 0.6 seconds, and resumes typing at 2.0 seconds.

Note: The time values you’ll see in the Debug area may be offset by one or two tenth of a second. Since you’ll be emitting values on the main queue using DispatchQueue.asyncAfter(), you are guaranteed a minimum time interval between values but maybe not exactly what you requested.

In the playground’s Debounce page, create two timelines to visualize events, and wire them up to the two publishers:

let subjectTimeline = TimelineView(title: "Emitted values")
let debouncedTimeline = TimelineView(title: "Debounced values")

let view = VStack(spacing: 100) {
	subjectTimeline
	debouncedTimeline
}

PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))

subject.displayEvents(in: subjectTimeline)
debounced.displayEvents(in: debouncedTimeline)

You are now familiar with this playground structure where you stack timelines on the screen and connect them to the publishers for event display.

This time, you’re going to do something more: Print values each publisher emits, along with the time (since start) at which they show up. This will help you figure out what’s happening.

Add this code:

let subscription1 = subject
  .sink { string in
    print("+\(deltaTime)s: Subject emitted: \(string)")
  }

let subscription2 = debounced
  .sink { string in
    print("+\(deltaTime)s: Debounced emitted: \(string)")
  }

Each subscription prints the values it receives, along with the time since start. deltaTime is a dynamic global variable defined in Sources/DeltaTime.swift which formats the time difference since the playground started running.

Now you need to feed your subject with data. This time you’re going to use a pre-made data source that simulates a user typing text. It’s all defined in Sources/Data.swift and you can modify it at will. Take a look, you’ll see that it’s a simulation of a user typing the words “Hello World”.

Add this code to the end of the playground page:

subject.feed(with: typingHelloWorld)

The feed(with:) method takes a data set and sends data to the given subject at pre-defined time intervals. A handy tool for simulations and mocking data input! You may want to keep this around when you write tests for your code because you will write tests, won’t you?

Now look at the result:

You see the emitted values at the top, there are 11 strings total being pushed to the sourcePublisher. You can see that the user paused between the two words. This is the time where debounce emitted the captured input.

You can confirm this by looking at the debug area where the prints show up:

+0.0s: Subject emitted: H
+0.1s: Subject emitted: He
+0.2s: Subject emitted: Hel
+0.3s: Subject emitted: Hell
+0.5s: Subject emitted: Hello
+0.6s: Subject emitted: Hello 
+1.6s: Debounced emitted: Hello 
+2.1s: Subject emitted: Hello W
+2.1s: Subject emitted: Hello Wo
+2.4s: Subject emitted: Hello Wor
+2.4s: Subject emitted: Hello Worl
+2.7s: Subject emitted: Hello World
+3.7s: Debounced emitted: Hello World

As you can see, at 0.6 seconds the user pauses and resumes typing only at 2.1 seconds. Meanwhile, you configured debounce to wait for a one-second pause. It obliges (at 1.6 seconds) and emits the latest received value.

Same around the end where typing ends at 2.7 seconds and debounce kicks in one second later at 3.7 seconds. Cool!

Note: One thing to watch out for is the publisher’s completion. If your publisher completes right after the last value was emitted, but before the time configured for debounce elapses, you will never see the last value in the debounced publisher!

Throttle

The kind of holding-off pattern that debounce allows is so useful that Combine provides a close relative: throttle(for:scheduler:latest:). It’s very close to debounce, but the differences justify the need for two operators.

Switch to the Throttle page in the playground and get coding. First, you need a constant, as usual:

let throttleDelay = 1.0

// 1
let subject = PassthroughSubject<String, Never>()

// 2
let throttled = subject
  .throttle(for: .seconds(throttleDelay), scheduler: DispatchQueue.main, latest: false)
  // 3
  .share()

Breaking down this code:

  1. The source publisher will emit strings.
  2. Your throttled subject will now only emit the first value it received from subject during each one-second interval because you set latest to false.
  3. Like in the previous operator, debounce, adding the share() operator here guarantees that all subscribers see the same output at the same time from the throttled subject.

Create timelines to visualize events, and wire them up to the two publishers:

let subjectTimeline = TimelineView(title: "Emitted values")
let throttledTimeline = TimelineView(title: "Throttled values")

let view = VStack(spacing: 100) {
  subjectTimeline
  throttledTimeline
}

PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))

subject.displayEvents(in: subjectTimeline)
throttled.displayEvents(in: throttledTimeline)

Now you also want to print the values each publisher emits, to better understand what’s going on. Add this code:

let subscription1 = subject
  .sink { string in
    print("+\(deltaTime)s: Subject emitted: \(string)")
  }

let subscription2 = throttled
  .sink { string in
    print("+\(deltaTime)s: Throttled emitted: \(string)")
  }

Again, you‘re going to feed your source publisher with a simulated “Hello World” user input. Add this final line to your playground page:

subject.feed(with: typingHelloWorld)

Your playground is ready! You can now see what’s happening in the live view:

Isn’t this puzzling? It doesn’t look that much different from the previous debounce output! Well, it actually is.

First, look closely at both. You can see that the values emitted by throttle have slightly different timing.

Second, to get a better picture of what’s happening, look at the debug console:

+0.0s: Subject emitted: H
+0.0s: Throttled emitted: H
+0.1s: Subject emitted: He
+0.2s: Subject emitted: Hel
+0.3s: Subject emitted: Hell
+0.5s: Subject emitted: Hello
+0.6s: Subject emitted: Hello 
+1.0s: Throttled emitted: He
+2.2s: Subject emitted: Hello W
+2.2s: Subject emitted: Hello Wo
+2.2s: Subject emitted: Hello Wor
+2.4s: Subject emitted: Hello Worl
+2.7s: Subject emitted: Hello World
+3.0s: Throttled emitted: Hello W

This is clearly different! You can see a few interesting things here:

  • When the subject emits its first value, throttle immediately relays it. Then, it starts throttling the output.
  • At 1.0 second, throttle emits “He”. Remember you asked it to send you the first value (since the last) after one second.
  • At 2.2 seconds, typing resumes. You can see that at this time, throttle didn’t emit anything. This is because no new value had been received from the source publisher.
  • At 3.0 seconds, after typing completes, throttle kicks in again and outputs the first value again, i.e., the value at 2.2 seconds.

There you have the fundamental difference between debounce and throttle:

  • debounce waits for a pause in values it receives, then emits the latest one after the specified interval.
  • throttle waits for the specified interval, then emits either the first or the latest of the values it received during that interval. It doesn’t care about pauses.

To see what happens when you change latest to true, change your setup of the throttled publisher to the following:

let throttled = subject
  .throttle(for: .seconds(throttleDelay), scheduler: DispatchQueue.main, latest: true)
  .share()

Now, observe the resulting output in the debug area:

+0.0s: Subject emitted: H
+0.0s: Throttled emitted: H
+0.1s: Subject emitted: He
+0.2s: Subject emitted: Hel
+0.3s: Subject emitted: Hell
+0.5s: Subject emitted: Hello
+0.6s: Subject emitted: Hello 
+1.0s: Throttled emitted: Hello
+2.0s: Subject emitted: Hello W
+2.3s: Subject emitted: Hello Wo
+2.3s: Subject emitted: Hello Wor
+2.6s: Subject emitted: Hello Worl
+2.6s: Subject emitted: Hello World
+3.0s: Throttled emitted: Hello World

The throttled output occurs at precisely 1.0 second and 3.0 seconds with the latest value in the time window instead of the earliest one. Compare this with the output from debounce from the earlier example:

...
+1.6s: Debounced emitted: Hello 
...
+3.7s: Debounced emitted: Hello World

The output is the same, but debounce is delayed from the pause.

Timing out

Next in this roundup of time manipulation operators is a special one: timeout. Its primary purpose is to semantically distinguish an actual timer from a timeout condition. Therefore, when a timeout operator fires, it either completes the publisher or emits an error you specify. In both cases, the publisher terminates.

Switch to the Timeout playground page. Begin by adding this code:

let subject = PassthroughSubject<Void, Never>()

// 1
let timedOutSubject = subject.timeout(.seconds(5), scheduler: DispatchQueue.main)
  1. The timedOutSubject publisher will time-out after five seconds without the upstream publisher emitting any value. This form of timeout forces a publisher completion without any failure.

You now need to add your timeline, as well as a button to let you trigger events:

let timeline = TimelineView(title: "Button taps")

let view = VStack(spacing: 100) {
  // 1
  Button(action: { subject.send() }) {
    Text("Press me within 5 seconds")
  }
  timeline
}

PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))

timedOutSubject.displayEvents(in: timeline)
  1. This is a new one! You add a button above the timeline, which sends a new value through the source subject when pressed. The action closure will execute every time you press the button.

Note: Have you noticed you’re using a subject that emits Void values? Yes, this is totally legitimate! It signals that something happened. But, there is no particular value to carry. So, you simply use Void as the value type. This is such a common case that Subject has an extension with a send() function that takes no parameter in case the Output type is Void. This saves you from writing the awkward subject.send(()) statement!

Your playground page is now complete. Watch it run and do nothing: the timeout will trigger after five seconds and complete the publisher.

Now, run it again. This time, keep pressing the button at less-than-five-seconds intervals. The publisher never completes because timeout doesn’t kick in.

Of course, the simple completion of a publisher is not what you want in many cases. Instead, you need the timeout publisher to send a failure so you can accurately take action in this case.

Go to the top of the playground page and define the error type you want:

enum TimeoutError: Error {
  case timedOut
}

Next, modify the definition of subject to change the error type from Never to TimeoutError. Your code should look like this:

let subject = PassthroughSubject<Void, TimeoutError>()

Now you need to modify the call to timeout. The complete signature for this operator is timeout(_:scheduler:options:customError:). Here is your chance to provide your custom error type!

Modify the line that creates the timedOutSubjectto this:

let timedOutSubject = subject.timeout(.seconds(5),
                                      scheduler: DispatchQueue.main,
                                      customError: { .timedOut })

Now when you run the playground and don’t press the button for five seconds, you can see that the timedOutSubject emits a failure.

Now that the time allocated to this operator ran out, let’s move to the last one in this section.

Measuring time

To complete this roundup of time manipulation operators, you’ll look at one particular operator which doesn’t manipulate time but just measures it. The measureInterval(using:) operator is your tool when you need to find out the time that elapsed between two consecutive values a publisher emitted.

Switch to the MeasureInterval playground page. Begin by creating a pair of publishers:

let subject = PassthroughSubject<String, Never>()

// 1
let measureSubject = subject.measureInterval(using: DispatchQueue.main)

The measureSubject will emit measurements on the scheduler you specify. Here, the main queue.

Now as usual, add a couple of timelines:

let subjectTimeline = TimelineView(title: "Emitted values")
let measureTimeline = TimelineView(title: "Measured values")

let view = VStack(spacing: 100) {
  subjectTimeline
  measureTimeline
}

PlaygroundPage.current.liveView = UIHostingController(rootView: view.frame(width: 375, height: 600))

subject.displayEvents(in: subjectTimeline)
measureSubject.displayEvents(in: measureTimeline)

Finally, here comes the interesting part. Print out values both publishers emit, and then feed the subject:

let subscription1 = subject.sink {
  print("+\(deltaTime)s: Subject emitted: \($0)")
}

let subscription2 = measureSubject.sink {
  print("+\(deltaTime)s: Measure emitted: \($0)")
}

subject.feed(with: typingHelloWorld)

Run your playground and have a look at the debug area! This is where you see what measureInterval(using:) emits:

+0.0s: Subject emitted: H
+0.0s: Measure emitted: Stride(magnitude: 16818353)
+0.1s: Subject emitted: He
+0.1s: Measure emitted: Stride(magnitude: 87377323)
+0.2s: Subject emitted: Hel
+0.2s: Measure emitted: Stride(magnitude: 111515697)
+0.3s: Subject emitted: Hell
+0.3s: Measure emitted: Stride(magnitude: 105128640)
+0.5s: Subject emitted: Hello
+0.5s: Measure emitted: Stride(magnitude: 228804831)
+0.6s: Subject emitted: Hello 
+0.6s: Measure emitted: Stride(magnitude: 104349343)
+2.2s: Subject emitted: Hello W
+2.2s: Measure emitted: Stride(magnitude: 1533804859)
+2.2s: Subject emitted: Hello Wo
+2.2s: Measure emitted: Stride(magnitude: 154602)
+2.4s: Subject emitted: Hello Wor
+2.4s: Measure emitted: Stride(magnitude: 228888306)
+2.4s: Subject emitted: Hello Worl
+2.4s: Measure emitted: Stride(magnitude: 138241)
+2.7s: Subject emitted: Hello World
+2.7s: Measure emitted: Stride(magnitude: 333195273)

The values are a bit puzzling, aren’t they? It turns out that, as per the documentation, the type of the value measureInterval emits is “the time interval of the provided scheduler”. In the case of DispatchQueue, the TimeInterval is defined as “A DispatchTimeInterval created with the value of this type in nanoseconds.”.

What you are seeing here is a count, in nanoseconds, between each consecutive value received from the source subject. You can now fix the display to show more readable values. Modify the code that prints values from measureSubject like so:

let subscription2 = measureSubject.sink {
  print("+\(deltaTime)s: Measure emitted: \(Double($0.magnitude) / 1_000_000_000.0)")
}

Now, you’ll see values in seconds.

But what happens if you use a different scheduler? You can try it using a RunLoop instead of a DispatchQueue!

Note: You will explore the RunLoop and DispatchQueue schedulers in depth in Chapter 17, “Schedulers.”

Back to the top of the file, create a second subject that uses a RunLoop:

let measureSubject2 = subject.measureInterval(using: RunLoop.main)

You don’t need to bother wiring up a new timeline view, because what’s interesting is the debug output. Add this third subscription to your code:

let subscription3 = measureSubject2.sink {
  print("+\(deltaTime)s: Measure2 emitted: \($0)")
}

Now, you’ll see the output from the RunLoop scheduler as well, with magnitudes directly expressed in seconds:

+0.0s: Subject emitted: H
+0.0s: Measure emitted: 0.016503769
+0.0s: Measure2 emitted: Stride(magnitude: 0.015684008598327637)
+0.1s: Subject emitted: He
+0.1s: Measure emitted: 0.087991755
+0.1s: Measure2 emitted: Stride(magnitude: 0.08793699741363525)
+0.2s: Subject emitted: Hel
+0.2s: Measure emitted: 0.115842671
+0.2s: Measure2 emitted: Stride(magnitude: 0.11583995819091797)
...

The scheduler you use for measurement is really up to your personal taste. It is generally a good idea to stick with DispatchQueue for everything. But that’s your personal choice!

Challenge

Challenge: Data

If time allows, you may want to try a little challenge to put this new knowledge to good use!

Open the starter challenge playground in the projects/challenge folder. You see some code waiting for you:

  • A subject that emits integers.
  • A function call that feeds the subject with mysterious data.

In between those parts, your challenge is to:

  • Group data by batches of 0.5 seconds.
  • Turn the grouped data into a string.
  • If there is a pause longer than 0.9 seconds in the feed, print the 👏 emoji. Hint: Create a second publisher for this step and merge it with the first publisher in your subscription.
  • Print it.

Note: To convert an Int to a Character, you can do something like Character(Unicode.Scalar(value)!).

If you code this challenge correctly, you’ll see a sentence printed in the Debug area. What is it?

Solution

You’ll find the solution to this challenge in the challenge/Final.playground Xcode playground.

Here’s the solution code:

// 1
let strings = subject
  // 2
  .collect(.byTime(DispatchQueue.main, .seconds(0.5)))
  // 3
  .map { array in
    String(array.map { Character(Unicode.Scalar($0)!) })
  }

// 4
let spaces = subject.measureInterval(using: DispatchQueue.main)
  .map { interval in
    // 5
    interval > 0.9 ? "👏" : ""
  }

// 6
let subscription = strings
  .merge(with: spaces)
  // 7
  .filter { !$0.isEmpty }
  .sink {
    // 8
    print($0)
  }

From the top, you:

  1. Create a first publisher derived from the subject which emits the strings.
  2. Use collect() using the .byTime strategy to group data in 0.5 seconds batches.
  3. Map each integer value to a Unicode scalar, then to a character and then turn the whole lot into a string using map.
  4. Create a second publisher derived from the subject, which measures the intervals between each character.
  5. If the interval is greater than 0.9 seconds, map the value to the 👏 emoji. Otherwise, map it to an empty string.
  6. The final publisher is a merge of both strings and the 👏 emoji.
  7. Filter out empty strings for better display.
  8. Print the result!

Your solution might have been subtly different, and that’s OK. As long as you met the requirements, you get the W!

Running the playground with this solution will print the following output to the console:

Combine
👏
is
👏
cool!

Key points

In this chapter, you looked at time from a different perspective. In particular, you learned that:

  • Combine’s handling of asynchronous events extends to manipulating time itself.
  • Even though it doesn’t provide time-traveling options, the framework has operators that let you abstract work over long periods of time, rather than just handling discrete events.
  • Time can be shifted using the delay operator.
  • You can manage the flow of values over time like a dam and release them by chunks using collect.
  • Picking individual values over time is easy with debounce and throttle.
  • Not letting time run out is the job of timeout.
  • Time can be measured with measureInterval.

Where to go from here?

This was a lot to learn. To put events in their right order, move along to the next chapter and learn about sequence operators!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.