What do debounce, throttle and removeDuplicates do, and what do share and multicast change?
Predict first
How many times does the upstream run with and without share() for two subscribers?
import Combine
var subs = 0
var bag = Set<AnyCancellable>()
let up = Deferred { subs += 1; return Just(1) }
up.sink { _ in }.store(in: &bag)
up.sink { _ in }.store(in: &bag)
print("plain", subs)
subs = 0
let shared = up.share()
shared.sink { _ in }.store(in: &bag)
shared.sink { _ in }.store(in: &bag)
print("shared", subs)
- plain 2, shared 1
- plain 1, shared 1
- plain 2, shared 2
Predict first
Three quick values go through debounce. What gets through?
import Combine
import Foundation
var bag = Set<AnyCancellable>()
let s = PassthroughSubject<Int, Never>()
var deb: [Int] = []
s.debounce(for: .milliseconds(50), scheduler: DispatchQueue.main).sink { deb.append($0) }.store(in: &bag)
s.send(1); s.send(2); s.send(3)
try? await Task.sleep(nanoseconds: 200_000_000)
print(deb)
- [1, 2, 3]
- [3]
- [1]
Predict first
removeDuplicates on a sequence with repeats:
import Combine
var bag = Set<AnyCancellable>()
var rd: [Int] = []
[1, 1, 2, 2, 1].publisher.removeDuplicates().sink { rd.append($0) }.store(in: &bag)
print(rd)
- [1, 2]
- [1, 2, 1]
- [1, 1, 2, 2, 1]
Mental model. debounce is a lift that waits until people stop arriving before closing the doors. throttle is a bus on a fixed timetable. share is one cook serving the same dish to everyone at the table.
Timing operators reshape when values pass. debounce(for:scheduler:) waits until the stream has been quiet for the interval and then emits only the last value (the right tool for “search after the user stops typing”: bursts of 1, 2, 3 within the interval produce just 3). throttle(for:scheduler:latest:) emits at most one value per interval (the first or the latest, by latest), a steady sample while the input is busy (scroll position, slider, location). removeDuplicates() drops values equal to the previous one, delay shifts them, timeout fails or completes if nothing arrives, and collect(n) or collect(.byTime) batches. All time operators need a scheduler (DispatchQueue.main, RunLoop.main), and they emit on it, which also decides the thread. Note that RunLoop.main timers pause during scrolling in the default mode, so for UI work that must keep running, use DispatchQueue.main or RunLoop.main with common modes.
Sharing: by default each subscriber runs the upstream again (two sinks on a network publisher make two requests). share() makes one upstream subscription and multicasts its values to all current subscribers, without replay, so a late subscriber misses earlier values. multicast(subject:) gives you the control: it returns a ConnectablePublisher that does nothing until you call connect(), so you can attach several subscribers first and then start once. For replay use multicast { CurrentValueSubject… } or the shareReplay pattern, or store the result in a @Published property. Always think of two questions for each pipeline: who runs the upstream, and who gets which values.
Each subscriber of a cold publisher runs the upstream. share() subscribes upstream once and shares it.
Only the last value survives, after the stream has been quiet for 50 ms.
Only consecutive repeats are removed, so the last 1 comes through again.
Common traps
- Two subscribers on a cold request publisher. It fires two requests;
share()if you want one. - Using
share()and expecting late subscribers to get the earlier values. It does not replay.
They might follow up with
- Debounce or throttle for a search field? Debounce, because only the final text matters.
- When do you use
multicastinstead ofshare? When you need to subscribe everyone first and thenconnect(), or to choose the subject (such as one that replays).
Related questions
- 11.2.2 How do flatMap and switchToLatest differ, and when do you use each?
- 11.4.1 What do receive(on:) and subscribe(on:) do, and how do you know which thread a stage runs on?
- 11.1.1 What are Publisher, Subscriber, Subscription and Subscriber.Input/Failure, and when does a pipeline actually…
← How do flatMap and switchToLatest differ, and when do you use each?