Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

3. Transforming Operators
Written by Marin Todorov

Having completed section 1, you’ve already learned a lot. You should feel pretty good about that accomplishment! You’ve laid a solid foundation on the fundamentals of Combine, and now you’re ready to build upon it.

In this chapter, you’re going to learn about one of the essential categories of operators in Combine: Transforming operators. You’ll use transforming operators all the time, to manipulate values coming from publishers into a format that is usable for your subscribers. As you’ll see, there are parallels between transforming operators in Combine and regular operators in the Swift standard library, such as map and flatMap.

By the end of this chapter, you’ll be transforming all the things!

Getting started

Open the starter playground for this chapter, which already has Combine imported and is ready for you to start coding.

Operators are publishers

In Combine, we call methods that perform an operation on values coming from a publisher “operators”.

Each Combine operator returns a publisher. Generally speaking, the publisher receives upstream events, manipulates them, and then sends the manipulated events downstream to consumers.

To simplify this concept, in this chapter you’ll focus on using operators and working with their output. Unless an operator’s purpose is to handle upstream errors, it will just re-publish said errors downstream.

Note: You’ll focus on transforming operators in this chapter, so error handling will not appear in each operator example. You’ll learn all about error handling in Chapter 16, “Error Handling.”

Collecting values

Publishers can emit individual values or collections of values. You’ll frequently work with collections, for example when you want to populate list or grid views. You’ll learn how to do this later in the book.


The collect operator provides a convenient way to transform a stream of individual values from a publisher into a single array. To help understand how this and all other operators you’ll learn about in this book, you’ll use marble diagrams.

Marble diagrams help visualize how operators work. The top line is the upstream publisher. The box represents the operator. And the bottom line is the subscriber, or more specifically, what the subscriber will receive after the operator manipulates the values coming from the upstream publisher.

The bottom line could also be another operator that receives the output from the upstream publisher, performs its operation, and sends those values downstream.

collect() 1 1 2 3 ( , , ) 3 2

This marble diagram depicts how collect buffers a stream of individual values until the upstream publisher completes. It then emits that array downstream.

Add this new example to your playground:

example(of: "collect") {
  ["A", "B", "C", "D", "E"].publisher
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

This code is not using the collect operator just yet. Run the playground, and you’ll see each value appears on a separate line followed by a completion event:

——— Example of: collect ———

Now use collect just before calling sink. Your code should now look like this:

["A", "B", "C", "D", "E"].publisher
  .sink(receiveCompletion: { print($0) },
        receiveValue: { print($0) })
  .store(in: &subscriptions)

Run the playground again and you’ll now see that sink receives a single array value, followed by the completion event:

——— Example of: collect ———
["A", "B", "C", "D", "E"]

Note: Be careful when working with collect() and other buffering operators that do not require specifying a count or limit. They will use an unbounded amount of memory to store received values as they won’t emit before the upstream finishes.

There are a few variations of the collect operator. For example, you can specify that you only want to receive up to a certain number of values, effectively chopping the upstream into “batches”.

Replace the following line:




Run the playground, and you’ll see the following output:

——— Example of: collect ———
["A", "B"]
["C", "D"]

The last value, E, is also an array. That’s because the upstream publisher completed before collect filled its prescribed buffer, so it sent whatever it had left as an array.

Mapping values

In addition to collecting values, you’ll often want to transform those values in some way. Combine offers several mapping operators for that purpose.


The first you’ll learn about is map, which works just like Swift’s standard map, except that it operates on values emitted from a publisher. In the marble diagram, map takes a closure that multiplies each value by 2.

2 4 6 map { $0 * 2 } 1 2 3

Notice how, unlike collect, this operator re-publishes values as soon as they are published by the upstream.

Add this new example to your playground:

example(of: "map") {
  // 1
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut
  // 2
  [123, 4, 56].publisher
    // 3
    .map {
      formatter.string(for: NSNumber(integerLiteral: $0)) ?? ""
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

Here’s the play-by-play:

  1. Create a number formatter to spell out each number.
  2. Create a publisher of integers.
  3. Use map, passing a closure that gets upstream values and returns the result of using the formatter to return the number’s spelled out string.

Run the playground, and you will see this output:

——— Example of: map ———
one hundred twenty-three

Mapping key paths

The map family of operators also includes three versions that can map into one, two, or three properties of a value using key paths. Their signatures are as follows:

  • map<T>(_:)
  • map<T0, T1>(_:_:)
  • map<T0, T1, T2>(_:_:_:)

The T represents the type of values found at the given key paths.

In the next example, you’ll use the Coordinate type and quadrantOf(x:y:) method defined in Sources/SupportCode.swift. Coordinate has two properties: x and y. quadrantOf(x:y:) takes x and y values as parameters and returns a string indicating the quadrant for the x and y values.

Note: Quadrants are part of coordinate geometry. For more information you can visit

Feel free to review these definitions if you’re interested, otherwise just use map(_:_:) with the following example to your playground:

example(of: "mapping key paths") {
  // 1
  let publisher = PassthroughSubject<Coordinate, Never>()
  // 2
    // 3
    .map(\.x, \.y)
    .sink(receiveValue: { x, y in
      // 4
        "The coordinate at (\(x), \(y)) is in quadrant",
        quadrantOf(x: x, y: y)
    .store(in: &subscriptions)
  // 5
  publisher.send(Coordinate(x: 10, y: -8))
  publisher.send(Coordinate(x: 0, y: 5))

In this example you’re using the version of map that maps into two properties via key paths.

Step-by-step, you:

  1. Create a publisher of Coordinates that will never emit an error.
  2. Begin a subscription to the publisher.
  3. Map into the x and y properties of Coordinate using their key paths.
  4. Print a statement that indicates the quadrant of the provide x and y values.
  5. Send some coordinates through the publisher.

Run the playground and the output from this subscription will be the following:

——— Example of: map key paths ———
The coordinate at (10, -8) is in quadrant 4
The coordinate at (0, 5) is in quadrant boundary


Several operators, including map, have a counterpart with a try prefix that takes a throwing closure. If you throw an error, the operator will emit that error downstream.

To try tryMap add this example to the playground:

example(of: "tryMap") {
  // 1
  Just("Directory name that does not exist")
    // 2
    .tryMap { try FileManager.default.contentsOfDirectory(atPath: $0) }
    // 3
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

Here’s what you just did, or at least tried to!

  1. Create a publisher of a string representing a directory name that does not exist.
  2. Use tryMap to attempt to get the contents of that nonexistent directory.
  3. Receive and print out any values or completion events.

Notice that you still need to use the try keyword when calling a throwing method.

Run the playground and observe that tryMap outputs a failure completion event with the appropriate “folder doesn’t exist” error (output abbreviated):

——— Example of: tryMap ———
failure(..."The folder “Directory name that does not exist” doesn't exist."...)

Flattening publishers

Though somewhat mysterious at first, the concept of flattening isn’t too complex to understand. You’ll learn everything about it by working through few select examples.


The flatMap operator flattens multiple upstream publishers into a single downstream publisher — or more specifically, flatten the emissions from those publishers.

The publisher returned by flatMap does not — and often will not — be of the same type as the upstream publishers it receives.

A common use case for flatMap in Combine is when you want to pass elements emitted by one publisher to a method that itself returns a publisher, and ultimately subscribe to the elements emitted by that second publisher.

Time to implement an example to see this in action. Add this new example:

example(of: "flatMap") {
  // 1
  func decode(_ codes: [Int]) -> AnyPublisher<String, Never> {
    // 2
        .compactMap { code in
          guard (32...255).contains(code) else { return nil }
          return String(UnicodeScalar(code) ?? " ")
        // 3
    // 4

From the top, you:

  1. Define a function that takes an array of integers, each representing an ASCII code, and returns a type-erased publisher of strings that never emits errors.
  2. Create a Just publisher that converts the character code into a string if it’s within the range of 0.255, which includes standard and extended printable ASCII characters.
  3. Join the strings together.
  4. Type erase the publisher to match the return type for the fuction.

Note: For more information about ASCII character codes, you can visit

With that handiwork completed, add this code to your current example to put that function and the flatMap operator to work:

// 5
[72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]
  // 6
  // 7
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

With this code, you:

  1. Create a secret message as an array of ASCII character codes, convert it to a publisher, and collect its emitted elements into a single array.
  2. Use flatMap to pass the array element to your decoder function.
  3. Subscribe to the elements emitted by the pubisher returned by decode(_:) and print out the values.

Run the playground, and you’ll see the following:

——— Example of: flatMap ———
Hello, World!

Recall the definition from earlier: flatMap flattens the output from all received publishers into a single publisher. This can pose a memory concern, because it will buffer as many publishers as you send it to update the single publisher it emits downstream.

To understand how to manage this, take a look at this marble diagram of flatMap:

P1 flatMap(maxPublishers: .max(2)) { $0.value } 1 1 4 P2 2 4 2 5 5 P3 3

In the diagram, flatMap receives three publishers: P1, P2, and P3. Each of these publishers has a value property that is also a publisher. flatMap emits the value publishers’ values from P1 and P2, but ignores P3 because maxPublishers is set to 2. You’ll get more practice working with flatMap and its maxPublishers parameter in Chapter 19, “Testing.”

You now have a handle on one of the most powerful operators in Combine. However, flatMap is not the only way to swap input with a different output. So, before wrapping up this chapter, you’ll learn a couple more useful operating for doing the ol’ switcheroo.

Replacing upstream output

Earlier in the map example, you worked with Foundation’s Formatter.string(for:) method. It produces an optional string, and you used the nil-coalescing operator (??) to replace a nil value with a non-nil value. Combine also includes an operator that you can use when you want to always deliver a value.


As depicted in the following marble diagram, replaceNil will receive optional values and replace nils with the value you specify:

2 1 3 replaceNil(with: 2) Optional(3) Optional(1) nil

Add this new example to your playground:

example(of: "replaceNil") {
  // 1
  ["A", nil, "C"].publisher
    .replaceNil(with: "-") // 2
    .sink(receiveValue: { print($0) }) // 3
    .store(in: &subscriptions)

What you just did:

  1. Create a publisher from an array of optional strings.
  2. Use replaceNil(with:) to replace nil values received from the upstream publisher with a new non-nil value.
  3. Print out the value.

Note: replaceNil(with:) has overloads which can confuse Swift into picking the wrong one for your use case. This results in the type remaining as Optional<String> instead of being fully unwrapped. The code above uses eraseToAnyPublisher() to work around that bug. You can learn more about this issue in the Swift forums:

Run the playground, and you will see the following:

——— Example of: replaceNil ———

There is a subtle but important difference between using the nil-coalescing operator ?? and replaceNil. The ?? operator can still result in an nil result, while replaceNil cannot. Change the usage of replaceNil to the following, and you will see the error caused by the wrong operator overload:

.replaceNil(with: "-" as String?)

Revert that change before moving on. This example also demonstrates how you can chain together multiple operators in a compositional way. This allows you to manipulate the values coming from the origin publisher to the subscriber in a wide variety of ways.


You can use the replaceEmpty(with:) operator to replace — or really, insert — a value if a publisher completes without emitting a value.

In the following marble diagram, the publisher completes without emitting anything, and at that point the replaceEmpty(with:) operator inserts a value and publishes it downstream:

1 replaceEmpty(with: 1)

Add this new example to see it in action:

example(of: "replaceEmpty(with:)") {
  // 1
  let empty = Empty<Int, Never>()
  // 2
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

What you’re doing here:

  1. Create an empty publisher that immediately emits a completion event.
  2. Subscribe to it, and print received events.

Use the Empty publisher type to create a publisher that immediately emits a .finished completion event. You could also configure it to never emit anything by passing false to its completeImmediately parameter, which is true by default. This publisher is useful for demo or testing purposes, or when all you want to do is signal completion of some task to a subscriber. Run the playground and you’ll see it successfully completes:

——— Example of: replaceEmpty ———

Now, insert this line of code before calling sink:

.replaceEmpty(with: 1)

Run the playground again, and this time you get a 1 before the completion:


Incrementally transforming output

You’ve seen how Combine includes operators such as map that correspond and work similarly to higher-order functions found in the Swift standard library. However, Combine has a few more tricks up its sleeve that let you manipulate values received from an upstream publisher.


A great example of this in the transforming category is scan. It will provide the current value emitted by an upstream publisher to a closure, along with the last value returned by that closure.

In the following marble diagram, scan begins by storing a starting value of 0. As it receives each value from the publisher, it adds it to the previously stored value, and then stores and emits the result:

1 3 6 scan(0) { $0 + $1 } 1 2 3

Note: If you are using the full project to enter and run this code, there’s no straightforward way to plot the output — as is possible in a playground. Instead, you can print the output by changing the sink code in the example below to .sink(receiveValue: { print($0) }).

For a practical example of how to use scan, add this new example to your playground:

example(of: "scan") {
  // 1
  var dailyGainLoss: Int { .random(in: -10...10) }

  // 2
  let august2019 = (0..<22)
    .map { _ in dailyGainLoss }

  // 3
    .scan(50) { latest, current in
      max(0, latest + current)
    .sink(receiveValue: { _ in })
    .store(in: &subscriptions)

In this example, you:

  1. Create a computed property that generates a random integer between -10 and 10.
  2. Use that generator to create a publisher from an array of random integers representing fictitious daily stock price changes for a month.
  3. Use scan with a starting value of 50, and then add each daily change to the running stock price. The use of max keeps the price non-negative — thankfully stock prices can’t fall below zero!

This time, you did not print anything in the subscription. Run the playground, and then click the square Show Results button in the right results sidebar.

Talk about a bull run! How’d your stock do?

There’s also an error-throwing tryScan operator that works similarly. If the closure throws an error, tryScan fails with that error.


Practice makes permanent. Complete this challenge to ensure you’re good to go with transforming operators before moving on.

Challenge: Create a phone number lookup using transforming operators

Your goal for this challenge is to create a publisher that does two things:

  1. Receives a string of ten numbers or letters.
  2. Looks up that number in a contacts data structure.

The starter playground, in the challenge folder, includes a contacts dictionary and three functions. You’ll need to create a subscription to the input publisher using transforming operators and those functions. Insert your code right below the Add your code here placeholder, before the forEach blocks that will test your implementation.

Tip: You can pass a function or closure directly to an operator as a parameter if the function signature matches. For example, map(convert).

Breaking down this challenge, you’ll need to:

  1. Convert the input to numbers — use the convert function, which will return nil if it cannot convert the input to an integer.
  2. If the previous operator returns nil, replace it with a 0.
  3. Collect ten values at a time, which correspond to the three-digit area code and seven-digit phone number format used in the United States.
  4. Format the collected string value to match the format of the phone numbers in the contacts dictionary — use the provided format function.
  5. “Dial” the input received from the previous operator — use the provided dial function.


Did your code produce the expected results? Starting with a subscription to input, first you needed to convert the string input one character at a time into integers:


Next you needed to replace nil values returned from convert with 0s:

.replaceNil(with: 0)

To look up the result of the previous operations, you needed to collect those values, and then format them to match the phone number format used in the contacts dictionary:


Finally, you needed to use the dial function to look up the formatted string input, and then subscribe:

.sink(receiveValue: { print($0) })

Running the playground will produce the following:

——— Example of: Create a phone number lookup ———
Contact not found for 000-123-4567
Dialing Marin (408-555-4321)...
Dialing Shai (212-555-3434)...

Bonus points if you hook this up to a VoIP service!

Key points

  • You call methods that perform operations on output from publishers “operators”.
  • Operators are also publishers.
  • Transforming operators convert input from an upstream publisher into output that is suitable for use downstream.
  • Marble diagrams are a great way to visualize how each Combine operators work.
  • Be careful when using any operators that buffer values such as collect or flatMap to avoid memory problems.
  • Be mindful when applying existing knowledge of functions from Swift standard library. Some similarly-named Combine operators work the same while others work entirely differently.
  • It’s common chaining multiple operators together in a subscription to create complex and compound transformations on events emitted by a publisher.

Where to go from here?

Way to go! You just transformed yourself into a transforming titan.

Now it’s time to learn how to use another essential collection of operators to filter what you get from an upstream publisher.

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.