Combining Publishers with Combine's Zip Operator

Combine Essentials

Combining Publishers with Combine's Zip Operator

The Combine framework defines a number of operators to combine two or more publishers into a single publisher. Each of these operators serves a specific purpose. In this episode, I show you how to use the zip operator to combine publishers.

Combining Publishers with Combine's Zip Operator

Fire up Xcode and create a playground by choosing the Blank template from the iOS section.

Combining Publishers with Combine's Zip Operator

Clear the contents of the playground and add an import statement for the Combine framework. We define two passthrough subjects. The first subject emits elements of type Int. The second subject emits elements of type String.

import Combine

let intSubject = PassthroughSubject<Int, Error>()
let stringSubject = PassthroughSubject<String, Error>()

We combine the subjects by applying the zip operator to the first subject, passing in the second subject.

import Combine

let intSubject = PassthroughSubject<Int, Error>()
let stringSubject = PassthroughSubject<String, Error>()

intSubject.zip(stringSubject)

It is also possible to combine publishers using the Publishers.Zip struct. The initializer of the Zip struct accepts the publishers you want to combine as arguments. It is up to you to decide which option you prefer.

import Combine

let intSubject = PassthroughSubject<Int, Error>()
let stringSubject = PassthroughSubject<String, Error>()

Publishers.Zip(intSubject, stringSubject)

We invoke the sink(receiveCompletion:receiveValue:) method on the resulting publisher. In the completion handler, we use a switch statement to switch on the Completion object. In the value handler, we print the value of the element the publisher emits.

import Combine

let intSubject = PassthroughSubject<Int, Error>()
let stringSubject = PassthroughSubject<String, Error>()

Publishers.Zip(intSubject, stringSubject)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure:
            print("Failure")
        }
    }, receiveValue: { value in
        print(value)
    })

The value of the element the publisher emits is a tuple, combining the values of the elements of the upstream publishers. In this example, the Output type of the zip publisher is a tuple of type (Int, String).

Let's take a look at the elements the zip publisher emits when the upstream publishers start emitting elements. The first subject starts by emitting an integer. Notice that nothing is printed to the console. In other words, the zip publisher doesn't emit an element. Even when the first subject emits a second element, the zip publisher still doesn't emit an element. The zip publisher emits its first element the moment the second subject emits an element. The first element the zip publisher publishes is a tuple consisting of the first element of the first subject and the first element of the second subject.

intSubject.send(1)
intSubject.send(2)
stringSubject.send("a")

// (1, "a")

If the first subject emits a third element, nothing happens.

intSubject.send(1)
intSubject.send(2)
stringSubject.send("a")
intSubject.send(3)

// (1, "a")

Let me explain what is happening. The zip operator creates a publisher that republishes the elements of its upstream publishers. As I explained earlier, the Output type of the publisher is a tuple composed of the elements of the upstream publishers, (Int, String) in this example.

What is unique about the zip operator is that it publishes its first element when every upstream publisher published an element. This makes sense if you understand how the zip operator works. It combines the elements of its upstream publishers, respecting the order in which the elements are emitted. This means that the first element of the first publisher is combined with the first element of the second publisher, the second element of the first publisher is combined with the second element of the second publisher, and so on.

That explains why the zip publisher emits a single tuple. Even though the first subject published three elements, the second subject published one element. Let me show you what happens if the second subject publishes another element.

intSubject.send(1)
intSubject.send(2)
stringSubject.send("a")
intSubject.send(3)
stringSubject.send("b")

// (1, "a")
// (2, "b")

According to the documentation, a zip publisher completes as soon as one of the upstream publishers emits a completion event or terminates with an error. Let's take a look at the example.

intSubject.send(1)
intSubject.send(2)
stringSubject.send("a")
intSubject.send(3)
stringSubject.send("b")
intSubject.send(completion: .finished)
intSubject.send(4)

// (1, "a")
// (2, "b")

This doesn't seem to be true and it may be a bug in the Combine framework. The completion event is sent as soon as the second subject sends another element, which isn't in line with what the documentation states.

intSubject.send(1)
intSubject.send(2)
stringSubject.send("a")
intSubject.send(3)
stringSubject.send("b")
intSubject.send(completion: .finished)
intSubject.send(4)
stringSubject.send("c")

// (1, "a")
// (2, "b")
// (3, "c")
// Finished

Publishing the Previous Value and the Current Value

The zip operator can be used to create a publisher that emits the previous element and the current element a publisher emits. We pass the same publisher to the initializer of the Publishers.Zip struct twice, but apply the dropFirst operator to the second publisher. This simply means that the second publisher doesn't emit the first element of the original publisher.

import Combine

let numbers = [1, 2, 3, 4, 5].publisher

Publishers.Zip(numbers, numbers.dropFirst(1))

The idea becomes clear if we apply the sink(receiveValue:) method on the zip publisher and print the elements it publishes to the console.

import Combine

let numbers = [1, 2, 3, 4, 5].publisher

Publishers.Zip(numbers, numbers.dropFirst(1))
    .sink(receiveValue: { values in
        print(values)
    })
    
// (1, 2)
// (2, 3)
// (3, 4)
// (4, 5)

This works because each upstream publisher needs to emit an element before the zip publisher emits its first element.

Zipping More Publishers

The Combine framework defines the Publishers.Zip3 and Publishers.Zip4 structs to combine three and four upstream publishers respectively. The zip operator also accepts up to three publishers, which means you can combine up to four upstream publishers using the zip operator.

publisher1.zip(publisher2, publisher3, publisher4)
    .sink(receiveValue: { values in
        print(values)
    })

Combine also defines a variant of the zip operator that accepts a closure as its second argument to transform the elements the zip publisher emits.

import Combine
import Foundation

let numbers = [1, 2, 3, 4, 5].publisher

numbers.zip(numbers.dropFirst(1)) { previous, current in
    previous + current
}
.sink { print($0) }

You can achieve the same result by combining the zip operator with the map operator.

numbers.zip(numbers.dropFirst(1))
    .map { previous, current in
        previous + current
    }
    .sink { print($0) }

What's Next?

The zip operator is one of the many operators to combine two or more publishers. It defines a clear relationship between the upstream publishers, that is, it combines the elements of its upstream publishers, respecting the order in which the elements are emitted.