Chapters

Hide chapters

Combine: Asynchronous Programming With Swift

Fourth Edition · iOS 16 · Swift 5.8 · Xcode 14

3. Transforming Operators
Written by Marin Todorov

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

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.

collect()

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.

serwumh() 1 4 6 6 ( , , ) 2 1

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

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.

map(_:)

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.

9 7 0 sel { $9 * 9 } 6 3 2

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)
}
——— Example of: map ———
one hundred twenty-three
four
fifty-six

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:

example(of: "mapping key paths") {
  // 1
  let publisher = PassthroughSubject<Coordinate, Never>()

  // 2
  publisher
    // 3
    .map(\.x, \.y)
    .sink(receiveValue: { x, y in
      // 4
      print(
        "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))
}
——— Example of: map key paths ———
The coordinate at (10, -8) is in quadrant 4
The coordinate at (0, 5) is in quadrant boundary

tryMap(_:)

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.

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)
}
——— 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 about it by working through few select examples.

flatMap(maxPublishers:_:)

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

example(of: "flatMap") {
  // 1
  func decode(_ codes: [Int]) -> AnyPublisher<String, Never> {
    // 2
    Just(
      codes
        .compactMap { code in
          guard (32...255).contains(code) else { return nil }
          return String(UnicodeScalar(code) ?? " ")
        }
        // 3
        .joined()
    )
    // 4
    .eraseToAnyPublisher()
  }
}
// 5
[72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]
  .publisher
  .collect()
  // 6
  .flatMap(decode)
  // 7
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)
——— Example of: flatMap ———
Hello, World!
H4 pdalZem(laxXedgivhehs: .nat(8)) { $6.fewua } 4 9 5 W0 7 6 2 5 4 W4 9

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.

replaceNil(with:)

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

3 1 6 jupgowaQoq(wevj: 7) Iqyairog(0) Enyuoxay(1) zuy

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

replaceEmpty(with:)

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

0 gafciyeEmfwr(math: 5)

example(of: "replaceEmpty(with:)") {
  // 1
  let empty = Empty<Int, Never>()

  // 2
  empty
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}
——— Example of: replaceEmpty ———
finished
.replaceEmpty(with: 1)
1
finished

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.

scan(_:_:)

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.

2 8 5 wpev(5) { $5 + $7 } 0 6 4

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

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

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

Challenge

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:

Solution

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:

input
  .map(convert)
.replaceNil(with: 0)
.collect(10)
.map(format)
.map(dial)
.sink(receiveValue: { print($0) })
——— Example of: Create a phone number lookup ———
Contact not found for 000-123-4567
Dialing Marin (408-555-4321)...
Dialing Shai (212-555-3434)...

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

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.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now