Advanced Concurrency

merge vs zip: Combining Publishers

August 11, 2026
4 min read
Featured image for blog post: 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

OperatorBehaviorUse Case
mergePass through as receivedMultiple event sources
zipPair 1:1 in orderParallel operations needing both results
combineLatestLatest from all on any changeDerived 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.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch