Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

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.

3 4 3 4 2 1 prepend(1, 2)

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:

  1. Create a publisher that emits the numbers 3 4.
  2. Use prepend to add the numbers 1 and 2 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.

3 4 3 4 2 1 prepend([1, 2])

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:

  1. Create a publisher that emits the numbers 5, 6 and 7.
  2. Chain prepend(Sequence) twice to the original publisher. Once to prepend values from an Array and a second time to prepend values from a Set.

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 Sets, as opposed to Arrays, 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 either 1 and 2, or 2 and 1.

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.

3 4 3 4 2 1 prepend(publisher2) 1 2 publisher2

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:

  1. Create two publishers. One emitting the numbers 3 and 4, and a second one emitting 1 and 2.
  2. Prepend publisher2 to the beginning of publisher1. publisher1 will start performing its work and emit events only after publisher2 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:

  1. Create two publishers. The first emits values 3, and 4 while the second is a PassthroughSubject that can accept values dynamically.
  2. Prepend the subject before publisher1.
  3. Send the values 1 and 2 through the subject publisher2.

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.

1 2 append(3, 4) 1 2 3 4

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:

  1. Create a publisher emitting only a single value: 1.
  2. Use append twice, first to append 2 and 3 and then to append 4.

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:

  1. publisher is now a PassthroughSubject, which lets you manually send values to it.
  2. You send 1 and 2 to the PassthroughSubject.

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.

1 2 append([3, 4]) 1 2 3 4

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:

  1. Create a publisher that emits 1, 2 and 3.
  2. Append an Array with the values 4 and 5 (ordered).
  3. Append a Set with the values 6 and 7 (unordered).
  4. Append a Strideable that strides between 8 and 11 by steps of 2.

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

1 2 1 2 3 4 append(publisher2) 3 4 publisher2

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:

  1. Create two publishers, where the first emits 1 and 2, and the second emits 3 and 4.
  2. Append publisher2 to publisher1, so all values from publisher2 are appended at the end of publisher1 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.

5 4 switchToLatest() Publisher<Publisher<Int,Error>,Error (publisher of publishers) numbers1 Cancelled numbers2 Cancelled 7 8 9 numbers2 4 6 5 1 2 9 8 7 numbers1 numbers3 1 3 2

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:

  1. Create three PassthroughSubjects that accept integers and no errors.
  2. Create a second PassthroughSubject that accepts other PassthroughSubjects. For example, you can send publisher1, publisher2 or publisher3 through it.
  3. Use switchToLatest on your publishers. Now, every time you send a different publisher through the publishers subject, you switch to the new one and cancel the previous subscription.
  4. Send publisher1 to publishers and then send 1 and 2 to publisher1.
  5. Send publisher2, which cancels the subscription to publisher1. You then send 3 to publisher1, but it’s ignored, and send 4 and 5 to publisher2, which are pushed through because there is an active subscription to publisher2.
  6. Send publisher3, which cancels the subscription to publisher2. As before, you send 6 to publisher2 and it’s ignored, and then send 7, 8 and 9, which are pushed through the subscription to publisher3.
  7. Finally, you send a completion event to the current publisher, publisher3, and another completion event to publishers. 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:

  1. Define a function, getImage(), which performs a network request to fetch a random image from Unsplash’s public API. This uses URLSession.dataTaskPublisher, one of the many Combine extensions for Foundation. You’ll learn much more about this and others in Section 3, “Combine in Action.”
  2. Create a PassthroughSubject to simulate user taps on a button.
  3. Upon a button tap, map the tap to a new network request for a random image by calling getImage(). This essentially transforms Publisher<Void, Never> into Publisher<Publisher<UIImage?, Never>, Never> — a publisher of publishers.
  4. 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.
  5. 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:

1 2 4 3 5 publisher2 1 2 3 4 5 merge(with: publisher2)

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:

  1. Create two PassthroughSubjects that accept and emit integer values and will not emit an error.
  2. Merge publisher1 with publisher2, interleaving the emitted values from both. Combine offers overloads that let you merge up to eight different publishers.
  3. You add 1 and 2 to publisher1, then add 3 to publisher2, then add 4 to publisher1 again and finally add 5 to publisher2.
  4. You send a completion event to both publisher1 and publisher2.

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.

1 2 3 “a” “b” “c” publisher2 combineLatest(publisher2) (2,”a”) (2,”b”) (3,”b”) (3,”c”)

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:

  1. Create two PassthroughSubjects. The first accepts integers with no errors, while the second accepts strings with no errors.
  2. Combine the latest emissions of publisher2 with publisher1. You may combine up to four different publishers using different overloads of combineLatest.
  3. Send 1 and 2 to publisher1, then "a" and "b" to publisher2, then 3 to publisher1 and finally "c" to publisher2.
  4. Send a completion event to both publisher1 and publisher2.

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.

1 2 3 “a” “b” “c” “d” publisher2 zip(publisher2) (1,”a”) (2,”b”) (3,”c”)

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:

  1. Create two PassthroughSubjects, where the first accepts integers and the second accepts strings. Both cannot emit errors.
  2. Zip publisher1 with publisher2, pairing their emissions once they each emit a new value.
  3. Send 1 and 2 to publisher1, then "a" and "b" to publisher2, then 3 to publisher1 again, and finally "c" and "d" to publisher2.
  4. Complete both publisher1 and publisher2.

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 and append 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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.