Problem Overview
The docs explain the differences between three approaches for combining Publishers:
Use combineLatest(_:)
when you want the downstream subscriber to
receive a tuple of the most-recent element from multiple publishers
when any of them emit a value. To pair elements from multiple
publishers, use zip(_:)
instead. To receive just the most-recent
element from multiple publishers rather than tuples, use merge(with:)
.
Zip
and CombineLatest
are the only appropriate solution for consuming events together in real-time. In your particular example (ordering of sending events) CombineLatest
is the best solution, as explained below.
CombineLatest
You are right that CombineLatest
does listen to events from nested Publishers, however prints Publisher
elements using a tuple. This can be easily fixed using map
, or compactMap
depending on the Array generic parameter. CombineLatest
publishes only the latest unconsumed events once both Publishers have published an element. This means that once both publishers have published an event, then all subsequent events will be published.
Listening Usage Demo
let a = CurrentValueSubject<[String], Never>(["a", "b", "c"])
let b = CurrentValueSubject<[String], Never>(["d", "e", "f"])
a
.combineLatest(b, +)
.sink { print("\($0)") }
.store(in: &cancellableSet)
b.send(["g", "h", "i"])
// ["a", "b", "c", "d", "e", "f"]
// ["a", "b", "c", "g", "h", "i"]
Merge
Publishers.Merge
may seem more appropriate because of the same Output
Generic type, however it isn't. Merge
only receives the latest published element for an individual Publisher
. So, we can't print the combined stream even though we've "merged" the Publishers. The documentation refers to Merge
as creating an interleaved stream rather than a combined Publisher.
Merge Listening Usage Demo:
let a = CurrentValueSubject<[String], Never>(["a", "b", "c"])
let b = CurrentValueSubject<[String], Never>(["d", "e", "f"])
let combined = Publishers.Merge(a, b)
combined.sink {
print($0)
}
.store(in: &cancellableSet)
b.send(["g", "h", "i"])
// ["a", "b", "c"]
// ["d", "e", "f"]
// ["g", "h", "i"]
Zip
Zip
is a viable alternative to CombineLatest
and both are valid options for printing combined Publisher events. The difference is that Zip
publishes the oldest unconsumed event when waiting for the other Publisher.
Listening Usage Demo
let a = CurrentValueSubject<[String], Never>(["a", "b", "c"])
let b = CurrentValueSubject<[String], Never>(["d", "e", "f"])
a
.zip(b).map(+)
.sink { print("\($0)") }
.store(in: &cancellableSet)
b.send(["g", "h", "i"])
// ["a", "b", "c", "d", "e", "f"]
// To print "g","h","i", we need `a` to send an event.
a.send(["a", "b", "c"])
// ["a", "b", "c", "g", "h", "i"]
Summary
Use combineLatest
for printing combined publisher events. Use zip
for printing combined publisher events when both publishers have to be in sync. Use merge
when you want to listen to both Publisher
events individually. Merge
works well for creating a single interleaved event stream. If either upstream publisher finishes successfully or fails with an error, the zipped/combined/merged publisher does the same. Carefully consider which operator to use for your particular application NOT only by type signature but by actual behaviour instead.
Bonus Notes
If you want to combine more than 4 Publishers
(why?), you can actually use nested Publishers.CombineLatest4
with another Publishers.CombineLatest
. SwiftUI uses this technique to combine more than 10 SwiftUI View
s in a single ViewBuilder
.
Code Example
let a = CurrentValueSubject<[String], Never>(["a", "b", "c"])
let b = CurrentValueSubject<[String], Never>(["d", "e", "f"])
let c = CurrentValueSubject<[String], Never>(["g", "h", "i"])
let d = CurrentValueSubject<[String], Never>(["j", "k", "l"])
let e = CurrentValueSubject<[String], Never>(["a", "b", "c"])
let f = CurrentValueSubject<[String], Never>(["d", "e", "f"])
let g = CurrentValueSubject<[String], Never>(["g", "h", "i"])
let h = CurrentValueSubject<[String], Never>(["j", "k", "l"])
let combinedOne = Publishers.CombineLatest4(a, b, c, d)
let combinedTwo = Publishers.CombineLatest4(e, f, g, h)
let combined = Publishers.CombineLatest(combinedOne, combinedTwo)