5.
Combining Operators
Written by Shai Mishali
Now that the transforming and filtering operator categories are in your tool belt, you have a substantial amount of knowledge. You’ve learned how operators work, how they manipulate the upstream and how to use them to construct logical publisher chains from your data.
In this chapter, you’ll learn about one of the more complex, yet useful, categories of operators: Combining operators. This set of operators lets you combine events emitted by different publishers and create meaningful combinations of data in your Combine code.
Why is combining useful? Think about a form with multiple inputs from the user — a username, a password and a checkbox. You’ll need to combine these different pieces of data to compose a single publisher with all of the information you need.
As you learn more about how each operator functions and how to select the right one for your needs, your code will become substantially more capable and your skills will allow you to unlock new levels of publisher composition.
Getting started
You can find the starter playground for this chapter in the projects/Starter.playground folder. Throughout this chapter, you’ll add code to your playground and run it to see how various operators create different combinations of publishers and their events.
Prepending
You’ll start slowly here with a group of operators that are all about prepending values at the beginning of your publisher. In other words, you’ll use them to add values that emit before any values from your original publisher.
In this section, you’ll learn about prepend(Output...)
, prepend(Sequence)
and prepend(Publisher)
.
prepend(Output…)
This variation of prepend
takes a variadic list of values using the ...
syntax. This means it can take any number of values, as long as they’re of the same Output
type as the original publisher.
Add the following code to your playground to experiment with the above example:
example(of: "prepend(Output...)") {
// 1
let publisher = [3, 4].publisher
// 2
publisher
.prepend(1, 2)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
In the above code, you:
- Create a publisher that emits the numbers
3
4
. - Use
prepend
to add the numbers1
and2
before the publisher’s own values.
Run your playground. You should see the following in your debug console:
——— Example of: prepend(Output...) ———
1
2
3
4
Pretty straightforward!
Hang on, do you remember how operators are chainable? That means you can easily add more than a single prepend
, if you’d like.
Below the following line:
.prepend(1, 2)
Add the following:
.prepend(-1, 0)
Run your playground again. you should see the following output:
——— Example of: prepend(Output...) ———
-1
0
1
2
3
4
Notice that the order of operations is crucial here. The last prepend affects the upstream first, meaning -1
and 0
are prepended, then 1
and 2
, and finally the original publisher’s values.
prepend(Sequence)
This variation of prepend
is similar to the previous one, with the difference that it takes any Sequence
-conforming object as an input. For example, it could take an Array
or a Set
.
Add the following code to your playground to experiment with this operator:
example(of: "prepend(Sequence)") {
// 1
let publisher = [5, 6, 7].publisher
// 2
publisher
.prepend([3, 4])
.prepend(Set(1...2))
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
In this code, you:
- Create a publisher that emits the numbers
5
,6
and7
. - Chain
prepend(Sequence)
twice to the original publisher. Once to prepend values from anArray
and a second time to prepend values from aSet
.
Run the playground. Your output should be similar to the following:
——— Example of: prepend(Sequence) ———
1
2
3
4
5
6
7
Note: An important fact to remember about
Set
s, as opposed toArray
s, is that they are unordered, so the order in which the items emit is not guaranteed. This means the first two values in the above example could be either1
and2
, or2
and1
.
But wait, there’s more! Many types conform to Sequence
in Swift, which lets you do some interesting things.
After the second prepend:
.prepend(Set(1...2))
Add the following line:
.prepend(stride(from: 6, to: 11, by: 2))
In this line of code, you create a Strideable
which lets you stride between 6
and 11
in steps of 2
. Since Strideable
conforms to Sequence
, you can use it in prepend(Sequence)
.
Run your playground one more time and take a look at the debug console:
——— Example of: prepend(Sequence) ———
6
8
10
1
2
3
4
5
6
7
As you can see, three new values are now prepended to the publisher before the previous output – 6
, 8
and 10
, the result of striding between 6
and 11
in steps of 2
.
prepend(Publisher)
The two previous operators prepended lists of values to an existing publisher. But what if you have two different publishers and you want to glue their values together? You can use prepend(Publisher)
to add values emitted by a second publisher before the original publisher’s values.
Try out the above example by adding the following to your playground:
example(of: "prepend(Publisher)") {
// 1
let publisher1 = [3, 4].publisher
let publisher2 = [1, 2].publisher
// 2
publisher1
.prepend(publisher2)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
In this code, you:
- Create two publishers. One emitting the numbers
3
and4
, and a second one emitting1
and2
. - Prepend
publisher2
to the beginning ofpublisher1
.publisher1
will start performing its work and emit events only afterpublisher2
sends a.finished
completion event.
If you run your playground, your debug console should present the following output:
——— Example of: prepend(Publisher) ———
1
2
3
4
As expected, the values 1
and 2
are emitted first from publisher2
; only then are 3
and 4
emitted by publisher1
.
There’s one more detail about this operator that you should be aware of, and it would be easiest to show with an example.
Add the following to the end of your playground:
example(of: "prepend(Publisher) #2") {
// 1
let publisher1 = [3, 4].publisher
let publisher2 = PassthroughSubject<Int, Never>()
// 2
publisher1
.prepend(publisher2)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
// 3
publisher2.send(1)
publisher2.send(2)
}
This example is similar to the previous one, except that publisher2
is now a PassthroughSubject
that you can push values to manually.
In the following example, you:
- Create two publishers. The first emits values
3
, and4
while the second is aPassthroughSubject
that can accept values dynamically. - Prepend the subject before
publisher1
. - Send the values
1
and2
through the subjectpublisher2
.
Take a second and run through this code inside your head. What do you expect the output to be?
Now, run the playground again and take a look at the debug console. You should see the following:
——— Example of: prepend(Publisher) #2 ———
1
2
Wait, what? Why are there only two numbers emitted here from publisher2
? You must be thinking… hey there, Shai, didn’t you just say values should prepend to the existing publisher?
Well, think about it — how can Combine know the prepended publisher, publisher2
, has finished emitting values? It doesn’t, since it has emitted values, but no completion event. For that reason, a prepended publisher must complete so Combine knows it’s time to switch to the primary publisher.
After the following line:
publisher2.send(2)
Add this one:
publisher2.send(completion: .finished)
Combine now knows it can handle emissions from publisher1
since publisher2
has finished its work.
Run your playground again; you should see the expected output this time around:
——— Example of: prepend(Publisher) #2 ———
1
2
3
4
Appending
This next set of operators deals with concatenating events emitted by publishers with other values. But in this case, you’ll deal with appending instead of prepending, using append(Output...)
, append(Sequence)
and append(Publisher)
. These operators work similarly to their prepend
counterparts.
append(Output…)
append(Output...)
works similarly to its prepend
counterpart: It also takes a variadic list of type Output
but then appends its items after the original publisher has completed with a .finished
event.
Add the following code to your playground to experiment with this operator:
example(of: "append(Output...)") {
// 1
let publisher = [1].publisher
// 2
publisher
.append(2, 3)
.append(4)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
In the code above, you:
- Create a publisher emitting only a single value:
1
. - Use
append
twice, first to append2
and3
and then to append4
.
Think about this code for a minute — what do you think the output will be?
Run the playground and check out the output:
——— Example of: append(Output...) ———
1
2
3
4
Appending works exactly like you’d expect, where each append
waits for the upstream to complete before adding its own work to it.
This means that the upstream must complete or appending would never occur since Combine couldn’t know the previous publisher has finished emitting all of its values.
To verify this behavior, add the following example:
example(of: "append(Output...) #2") {
// 1
let publisher = PassthroughSubject<Int, Never>()
publisher
.append(3, 4)
.append(5)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
// 2
publisher.send(1)
publisher.send(2)
}
This example is identical to the previous one, with two differences:
-
publisher
is now aPassthroughSubject
, which lets you manually send values to it. - You send
1
and2
to thePassthroughSubject
.
Run your playground again and you’ll see that only the values sent to publisher
are emitted:
——— Example of: append(Output...) #2 ———
1
2
Both append
operators have no effect since they can’t possibly work until publisher
completes. Add the following line at the very end of the example:
publisher.send(completion: .finished)
Run your playground again and you should see all values, as expected:
——— Example of: append(Output...) #2 ———
1
2
3
4
5
This behavior is identical for the entire family of append
operators; no appending occurs unless the previous publisher sends a .finished
completion event.
append(Sequence)
This variation of append
takes any Sequence
-conforming object and appends its values after all values from the original publisher have emitted.
Add the following to your playground to experiment with this operator:
example(of: "append(Sequence)") {
// 1
let publisher = [1, 2, 3].publisher
publisher
.append([4, 5]) // 2
.append(Set([6, 7])) // 3
.append(stride(from: 8, to: 11, by: 2)) // 4
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
This code is similar to the prepend(Sequence)
example from the previous section. You:
- Create a publisher that emits
1
,2
and3
. - Append an
Array
with the values4
and5
(ordered). - Append a
Set
with the values6
and7
(unordered). - Append a
Strideable
that strides between8
and11
by steps of2
.
Run your playground and you should see the following output:
——— Example of: append(Sequence) ———
1
2
3
4
5
7
6
8
10
As you can see, the execution of the append
s is sequential as the previous publisher must complete before the next append
performs. Note that the set of 6
and 7
may be in a different order for you, as sets are unordered.
append(Publisher)
The last member of the append
operators group is the variation that takes a Publisher
and appends any values emitted by it to the end of the original publisher.
To try this example, add the following to your playground:
example(of: "append(Publisher)") {
// 1
let publisher1 = [1, 2].publisher
let publisher2 = [3, 4].publisher
// 2
publisher1
.append(publisher2)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
In this code, you:
- Create two publishers, where the first emits
1
and2
, and the second emits3
and4
. - Append
publisher2
topublisher1
, so all values frompublisher2
are appended at the end ofpublisher1
once it completes.
Run the playground and you should see the following output:
——— Example of: append(Publisher) ———
1
2
3
4
Advanced combining
At this point, you know everything about appending and prepending values, sequences and even entire publishers.
This next section will dive into some of the more complex operators related to combining different publishers. Even though they’re relatively complex, they’re also some of the most useful operators for publisher composition. It’s worth taking the time to get comfortable with how they work.
switchToLatest
Since this section includes some of the more complex combining operators in Combine, why not start with the most complex one of the bunch?!
Joking aside, switchToLatest
is complex but highly useful. It lets you switch entire publisher subscriptions on the fly while canceling the pending publisher subscription, thus switching to the latest one.
You can only use it on publishers that themselves emit publishers.
Add the following code to your playground to experiment with the example you see in the above diagram:
example(of: "switchToLatest") {
// 1
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let publisher3 = PassthroughSubject<Int, Never>()
// 2
let publishers = PassthroughSubject<PassthroughSubject<Int, Never>, Never>()
// 3
publishers
.switchToLatest()
.sink(
receiveCompletion: { _ in print("Completed!") },
receiveValue: { print($0) }
)
.store(in: &subscriptions)
// 4
publishers.send(publisher1)
publisher1.send(1)
publisher1.send(2)
// 5
publishers.send(publisher2)
publisher1.send(3)
publisher2.send(4)
publisher2.send(5)
// 6
publishers.send(publisher3)
publisher2.send(6)
publisher3.send(7)
publisher3.send(8)
publisher3.send(9)
// 7
publisher3.send(completion: .finished)
publishers.send(completion: .finished)
}
Yikes, that’s a lot of code! But don’t worry, it’s simpler than it looks. Breaking it down, you:
- Create three
PassthroughSubject
s that accept integers and no errors. - Create a second
PassthroughSubject
that accepts otherPassthroughSubject
s. For example, you can sendpublisher1
,publisher2
orpublisher3
through it. - Use
switchToLatest
on yourpublishers
. Now, every time you send a different publisher through thepublishers
subject, you switch to the new one and cancel the previous subscription. - Send
publisher1
topublishers
and then send1
and2
topublisher1
. - Send
publisher2
, which cancels the subscription topublisher1
. You then send3
topublisher1
, but it’s ignored, and send4
and5
topublisher2
, which are pushed through because there is an active subscription topublisher2
. - Send
publisher3
, which cancels the subscription topublisher2
. As before, you send6
topublisher2
and it’s ignored, and then send7
,8
and9
, which are pushed through the subscription topublisher3
. - Finally, you send a completion event to the current publisher,
publisher3
, and another completion event topublishers
. This completes all active subscriptions.
If you followed the above diagram, you might have already guessed the output of this example.
Run the playground and look at the debug console:
——— Example of: switchToLatest ———
1
2
4
5
7
8
9
Completed!
If you’re not sure why this is useful in a real-life app, consider the following scenario: Your user taps a button that triggers a network request. Immediately afterward, the user taps the button again, which triggers a second network request. But how do you get rid of the pending request, and only use the latest request? switchToLatest
to the rescue!
Instead of just theorizing, why don’t you try out this example?
Add the following code to your playground:
example(of: "switchToLatest - Network Request") {
let url = URL(string: "https://source.unsplash.com/random")!
// 1
func getImage() -> AnyPublisher<UIImage?, Never> {
URLSession.shared
.dataTaskPublisher(for: url)
.map { data, _ in UIImage(data: data) }
.print("image")
.replaceError(with: nil)
.eraseToAnyPublisher()
}
// 2
let taps = PassthroughSubject<Void, Never>()
taps
.map { _ in getImage() } // 3
.switchToLatest() // 4
.sink(receiveValue: { _ in })
.store(in: &subscriptions)
// 5
taps.send()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
taps.send()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.1) {
taps.send()
}
}
As in the previous example, this might look like a long and complicated piece of code, but it’s simple once you break it down.
In this code, you:
- Define a function,
getImage()
, which performs a network request to fetch a random image from Unsplash’s public API. This usesURLSession.dataTaskPublisher
, one of the many Combine extensions for Foundation. You’ll learn much more about this and others in Section 3, “Combine in Action.” - Create a
PassthroughSubject
to simulate user taps on a button. - Upon a button tap, map the tap to a new network request for a random image by calling
getImage()
. This essentially transformsPublisher<Void, Never>
intoPublisher<Publisher<UIImage?, Never>, Never>
— a publisher of publishers. - Use
switchToLatest()
exactly like in the previous example, since you have a publisher of publishers. This guarantees only one publisher will emit values, and will automatically cancel any leftover subscriptions. - Simulate three delayed button taps using a
DispatchQueue
. The first tap is immediate, the second tap comes after three seconds, and the last tap comes just a tenth of a second after the second tap.
Run the playground and take a look at the output below:
——— Example of: switchToLatest - Network Request ———
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive value: (Optional(<UIImage:0x600000364120 anonymous {1080, 720}>))
image: receive finished
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive cancel
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive value: (Optional(<UIImage:0x600000378d80 anonymous {1080, 1620}>))
image: receive finished
You might notice that only two images are actually fetched; that’s because only a tenth of a second passes between the second and third taps. The third tap switches to a new request before the second fetch returns, canceling the second subscription – hence the line that says image: receive cancel
.
If you want to see a better visualization of this, tap the following button:
Then run the playground again and wait a few seconds. You should see the last image loaded.
Right-click the image and select Value History:
You should see both loaded images — you may have to scroll to see both of them:
Before moving to the next operator, be sure to comment out this entire example to avoid running the asynchronous network requests every time you run your playground.
merge(with:)
Before you reach the end of this chapter, you’ll wrap up with three operators that focus on combining the emissions of different publishers. You’ll start with merge(with:)
.
This operator interleaves emissions from different publishers of the same type, like so:
To try out this example, add the following code to your playground:
example(of: "merge(with:)") {
// 1
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
// 2
publisher1
.merge(with: publisher2)
.sink(
receiveCompletion: { _ in print("Completed") },
receiveValue: { print($0) }
)
.store(in: &subscriptions)
// 3
publisher1.send(1)
publisher1.send(2)
publisher2.send(3)
publisher1.send(4)
publisher2.send(5)
// 4
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)
}
In this code, which correlates with the above diagram, you:
- Create two
PassthroughSubject
s that accept and emit integer values and will not emit an error. - Merge
publisher1
withpublisher2
, interleaving the emitted values from both. Combine offers overloads that let you merge up to eight different publishers. - You add
1
and2
topublisher1
, then add3
topublisher2
, then add4
topublisher1
again and finally add5
topublisher2
. - You send a completion event to both
publisher1
andpublisher2
.
Run your playground and you should see the following output, as expected:
——— Example of: merge(with:) ———
1
2
3
4
5
Completed
combineLatest
combineLatest
is another operator that lets you combine different publishers. It also lets you combine publishers of different value types, which can be extremely useful. However, instead of interleaving the emissions of all publishers, it emits a tuple with the latest values of all publishers whenever any of them emit a value.
One catch though: The origin publisher and every publisher passed to combineLatest
must emit at least one value before combineLatest
itself will emit anything.
Add the following code to your playground to try out this operator:
example(of: "combineLatest") {
// 1
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
// 2
publisher1
.combineLatest(publisher2)
.sink(
receiveCompletion: { _ in print("Completed") },
receiveValue: { print("P1: \($0), P2: \($1)") }
)
.store(in: &subscriptions)
// 3
publisher1.send(1)
publisher1.send(2)
publisher2.send("a")
publisher2.send("b")
publisher1.send(3)
publisher2.send("c")
// 4
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)
}
This code reproduces the above diagram. You:
- Create two
PassthroughSubjects
. The first accepts integers with no errors, while the second accepts strings with no errors. - Combine the latest emissions of
publisher2
withpublisher1
. You may combine up to four different publishers using different overloads ofcombineLatest
. - Send
1
and2
topublisher1
, then"a"
and"b"
topublisher2
, then3
topublisher1
and finally"c"
topublisher2
. - Send a completion event to both
publisher1
andpublisher2
.
Run the playground and take a look at the output in your console:
——— Example of: combineLatest ———
P1: 2, P2: a
P1: 2, P2: b
P1: 3, P2: b
P1: 3, P2: c
Completed
You might notice that the 1
emitted from publisher1
is never pushed through combineLatest
. That’s because combineLatest
only starts emitting combinations once every publisher emits at least one value. Here, that condition is true only after "a"
emits, at which point the latest emitted value from publisher1
is 2
. That’s why the first emission is (2, "a")
.
zip
You’ll finish with one final operator for this chapter: zip
. You might recognize this one from the Swift standard library method with the same name on Sequence
types.
This operator works similarly, emitting tuples of paired values in the same indexes. It waits for each publisher to emit an item, then emits a single tuple of items after all publishers have emitted an value at the current index.
This means that if you are zipping two publishers, you’ll get a single tuple emitted every time both publishers emit a new value.
Add the following code to your playground to try this example:
example(of: "zip") {
// 1
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
// 2
publisher1
.zip(publisher2)
.sink(
receiveCompletion: { _ in print("Completed") },
receiveValue: { print("P1: \($0), P2: \($1)") }
)
.store(in: &subscriptions)
// 3
publisher1.send(1)
publisher1.send(2)
publisher2.send("a")
publisher2.send("b")
publisher1.send(3)
publisher2.send("c")
publisher2.send("d")
// 4
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)
}
In this final example, you:
- Create two
PassthroughSubject
s, where the first accepts integers and the second accepts strings. Both cannot emit errors. - Zip
publisher1
withpublisher2
, pairing their emissions once they each emit a new value. - Send
1
and2
topublisher1
, then"a"
and"b"
topublisher2
, then3
topublisher1
again, and finally"c"
and"d"
topublisher2
. - Complete both
publisher1
andpublisher2
.
Run your playground a final time and take a look at the debug console:
——— Example of: zip ———
P1: 1, P2: a
P1: 2, P2: b
P1: 3, P2: c
Completed
Notice how each emitted value “waits” for the other zipped publisher to emit a value. 1
waits for the first emission from the second publisher, so you get (1, "a")
. Likewise, 2
waits for the next emission from the second publisher, so you get (2, "b")
. The last emitted value from the second publisher, "d"
, is ignored since there is no corresponding emission from the first publisher to pair with.
Key points
In this chapter, you learned how to take different publishers and create meaningful combinations from them. More specifically, you learned that:
- You can use the
prepend
andappend
families of operators to add emissions from one publisher before or after a different publisher. - While
switchToLatest
is relatively complex, it’s extremely useful. It takes a publisher that emits publishers, switches to the latest publisher and cancels the subscription to the previous publisher. -
merge(with:)
lets you interleave values from multiple publishers. -
combineLatest
emits the latest values of all combined publishers whenever any of them emit a value, once all of the combined publishers have emitted at least one value. -
zip
pairs emissions from different publishers, emitting a tuple of pairs after all publishers have emitted an value. - You can mix combination operators to create interesting and complex relationships between publishers and their emissions.
Where to go from here?
This has been quite a long chapter, but it includes some of the most useful and involved operators Combine has to offer. Kudos to you for making it this far!
No challenges this time. Try to experiment with all of the operators you’ve learned thus far, there are plenty of use cases to play with.
You have two more groups of operators to learn about in the next two chapters: “Time Manipulation Operators” and “Sequence Operators,” so move on to the next chapter!