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.
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.