merge vs zip: Combining Publishers

Both merge and zip combine publishers, but they work very differently. Here's when to use each.
Follow along with the code: iOS-Practice on GitHub
merge: Multiple Sources, One Stream
merge combines publishers of the SAME type into a single stream. Values pass through as they arrive.
let taps1 = PassthroughSubject<String, Never>()
let taps2 = PassthroughSubject<String, Never>()
let taps3 = PassthroughSubject<String, Never>()
Publishers.Merge3(taps1, taps2, taps3)
.sink { print("Tapped: \($0)") }
.store(in: &cancellables)
taps1.send("Button 1") // Prints: Tapped: Button 1
taps2.send("Button 2") // Prints: Tapped: Button 2
taps1.send("Button 1") // Prints: Tapped: Button 1
Use merge when:
- Multiple event sources of the same type
- You don't care which source emitted
- Order is "as they arrive"
zip: Pair Values 1:1
zip waits for BOTH publishers to emit, then pairs them in order.
let names = PassthroughSubject<String, Never>()
let ages = PassthroughSubject<Int, Never>()
Publishers.Zip(names, ages)
.sink { print("\($0) is \($1) years old") }
.store(in: &cancellables)
names.send("Alice") // Nothing yet, waiting for age
ages.send(30) // Prints: Alice is 30 years old
names.send("Bob") // Nothing, waiting for age
names.send("Carol") // Nothing, waiting for age (Bob is first in queue)
ages.send(25) // Prints: Bob is 25 years old
ages.send(35) // Prints: Carol is 35 years old
Use zip when:
- Pairing related values from separate streams
- Parallel requests that need to complete together
- Strict 1:1 correspondence required
Visual Comparison
merge: Events pass through as they arrive
A: ----1--------3-------->
B: -------2----------4--->
Out:----1--2-----3----4--->
zip: Waits to pair values in order
A: ----1--------3-------->
B: -------2----------4--->
Out:-------(1,2)-----(3,4)->
Parallel API Requests with zip
Fetch user and posts simultaneously, wait for both:
let userPublisher = api.fetchUser(id: 1)
let postsPublisher = api.fetchPosts(userId: 1)
Publishers.Zip(userPublisher, postsPublisher)
.sink { user, posts in
// Both requests completed
display(user: user, posts: posts)
}
.store(in: &cancellables)
merge Variants
Publishers.Merge(pub1, pub2) // 2 publishers
Publishers.Merge3(pub1, pub2, pub3) // 3 publishers
Publishers.Merge4(...) // 4 publishers
// ... up to Merge8
// For arrays of publishers
Publishers.MergeMany(arrayOfPublishers)
zip Variants
Publishers.Zip(pub1, pub2) // 2 publishers
Publishers.Zip3(pub1, pub2, pub3) // 3 publishers
Publishers.Zip4(pub1, pub2, pub3, pub4) // 4 publishers
Common Mistake: Using zip When You Mean combineLatest
// WRONG: zip waits for new values from BOTH
// If user rarely changes, you'll miss post updates
Publishers.Zip($currentUser, $latestPosts)
// RIGHT: combineLatest uses latest from each
Publishers.CombineLatest($currentUser, $latestPosts)
Quick Reference
| Operator | Behavior | Use Case |
|---|---|---|
merge | Pass through as received | Multiple event sources |
zip | Pair 1:1 in order | Parallel operations needing both results |
combineLatest | Latest from all on any change | Derived state |
Interview Tip
When asked about combining publishers, clarify the requirements: "Do you need to pair them strictly (zip), merge into one stream (merge), or react to any change (combineLatest)?" This shows you understand the semantic differences.