Leave a rating/review
When designing user interfaces you want to update some views when a user enters data in a text field. However, you don’t want to respond to every single keystroke the user makes. You want some mechanism to wait until the user stops typing. This is where debounce comes into play. Let’s look at debounce in a demo.
Starting in the debounce page, make a PassthroughSubject. Use debounce on the subject 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.
// 1
let subject = PassthroughSubject<String, Never>()
// 2
let debounced = subject
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
// 3
.share()
Since you are going to subscribe multiple times to debounced, and you want to be consistent, use share() to create a single subscription point to debounce that will show the same results at the same time to all subscribers. In the built in source code in the playground, there is an array that will help simulate a user typing.
Make timeline views to help visualize the sequence of values emitted from the publishers. Place them in a VStack and set the liveView for the playground.
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)
Send events to the timeline views from the publishers.
subject.displayEvents(in: subjectTimeline)
debounced.displayEvents(in: debouncedTimeline)
For this demo, you’ll add one more thing to the playground. Add 2 subscriptions that will print out the values to the console.
let subscription1 = subject
.sink { string in
print("+\(deltaTime)s: Subject emitted: \(string)")
}
let subscription2 = debounced
.sink { string in
print("+\(deltaTime)s: Debounced emitted: \(string)")
}
Finally, feed the array of simulated typing to the subject to start the process.
subject.feed(with: typingHelloWorld)
Run the playground. You see the visual displays in the playground live view, as well as the values in the console. Both the debounced timeline view and the console show that the values got debounced after the user types “Hello” and “World”
Throttle is similar to debounce functionally, but with some key differences. You can delay for a certain amount of time, as you did with debounce. You can also emit values to a particular scheduler. You can specify whether to emit the latest value received from the subject. Let’s look at throttle in a demo.
Define a throttle delay constant that you’ll use later. Then make a PassthroughSubject which emits strings.
let throttleDelay = 1.0
// 1
let subject = PassthroughSubject<String, Never>()
Apply the throttle operation to the subject.
// 2
let throttled = subject
.throttle(for: .seconds(throttleDelay), scheduler: DispatchQueue.main, latest: false)
// 3
.share()
With these settings on the throttle call, the throttled subject will now only emit the first value received from the subject during each one-second interval because you set latest to false. Remember that adding the share() operator here guarantees that all subscribers see the same output at the same time from the throttled subject.
Make TimelineViews to visualize the original and throttled publishers. Set the liveView of the playground, and tell each publisher to display events in the respective timeline views.
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)
subject.displayEvents(in: subjectTimeline)
throttled.displayEvents(in: throttledTimeline)
To help understand what is going on, make another subscription to print out the values emitted by each publisher.
let subscription1 = subject
.sink { string in
print("+\(deltaTime)s: Subject emitted: \(string)")
}
let subscription2 = throttled
.sink { string in
print("+\(deltaTime)s: Throttled emitted: \(string)")
}
Feed the subject with the typingHelloWorld array that simulates a user typing.
subject.feed(with: typingHelloWorld)
Run the playground.
It doesn’t look too much different than debounce. The console however shows a different picture. You see an extra throttle emission around 2.2 seconds, showing “Hello W”, which was the first value emitted by the subject since the last throttle. You also see a similar emission at 1.1 seconds, showing “He”.
Finally, change the argument of latest to true. Run the playground.
let throttled = subject
.throttle(for: .seconds(throttleDelay), scheduler: DispatchQueue.main, latest: true)
.share()
The output looks familiar doesn’t it? Values are emitted at 1.1 and 3.2 seconds - “Hello” and “Hello World”. This is like debounce, but debounce is delayed from the pause. Also take note that the value emitted by the throttle at approximately 2.2 seconds is now “Hello Wo”, including the latest value emitted from the subject publisher since the last time a throttle was emitted.
We looked at 2 similar timing operators in this episode: debounce allows you to wait for values to accumulate before passing them downstream to a consumer; throttle is like debounce, but lets you run on other schedulers and emit either the first or last value emitted from the publisher
You’ve got 2 more timing operators to learn about in the next episode - timeout and measureInterval. See you then.