Reactive Programming in iOS with Combine

Feb 4 2021 · Swift 5.3, macOS 11.0, Xcode 12.2

Part 2: Transforming & Filtering Operators

08. More Transforming Operators

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 07. Transforming Operators Next episode: 09. Challenge: Create a Phone Number Lookup

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 08. More Transforming Operators

We’ve convered some of the basic transforming operators, so let’s take a look at a few more that are a tad more complex.

Sometimes you want to build upon the previous results of an operator, and that’s where the scan operator comes into play. This operator takes the next value from the publisher as well as the most recent value returned from the closure. As you can see in the marble diagram, when the first value comes in, there is no recent value from the closure so the first value is emitted. When the next value comes in (“2”), that value as well as the most recent value from the closure (“1”) comes in, for an emitted value of 3. Let’s look at this in an example.

Add this code to your playground:

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)
}

Here, we have a computed property that returns a random number between -10 and 10. Then, we use that to create a publisher from an array of random integers, representing a stock price. Finally, the scan operator with a starting value of 50 is used on the publisher to simulate a daily change to the stock price. The use of max here keeps the stock price positive. Running the playground and expanding the result inline in the playground shows the stock behavior.

One more transforming operator is flatMap. Sometimes you want to take the output from more than one upstream publisher and combine them for use in a subscriber downstream. That’s where flatMap comes into play.

flatMap does not have to - and often will not have to - be of the same type as the upstream publishers it receives. A common case for flatMap in Combine is to subscribe to properties of values emitted by a publisher that are themselves publishers. Let’s look at an example, and afterwards you’ll look at the marble diagram.

If you take a look at the playground’s support file, you’ll see the definition of the Chatter struct

It has 2 properties, one of which is the name, a String, and the other is a message, which is a CurrentValueSubject - a Publisher. The passed in message string to the Chatter struct is used to initialize the Publisher. Going back to the main playground, add this code:

example(of: "flatMap") {
  // 1
  let charlotte = Chatter(name: "Charlotte", message: "Hi, I'm Charlotte!")
  let james = Chatter(name: "James", message: "Hi, I'm James!")

  // 2
  let chat = CurrentValueSubject<Chatter, Never>(charlotte)

  // 3
  chat
    .sink(receiveValue: { print($0.message.value) })
    .store(in: &subscriptions)
}

Here, you’ve made 2 Chatter instances - one for Charlotte, the other for James. Then you created a chat publisher, initialized with the charlotte publisher. Finally, you’ve attached a sink subscriber to the chat publisher, to output the message. Run the playground and see the message for charlotte is printed to the console.

Now add the following to the playground:

charlotte.message.value = "Charlotte: How's it going?"

chat.value = james

This time you’ll see Charlotte’s and James’ introductory message, but not the new value for Charlotte’s message.

This is because we’re subscribed to the chat publisher, which is a Chatter publisher. We’re not subscribed to the message publisher of each Chatter struct. Here’s where flatMap comes into play. Replace the chat/sink/store block of code with the following

chat
  // 6
  .flatMap { $0.message }
  // 7
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

Here we’re flatMapping into the chatter structure’s message property, and updating the handler to print out the value, which is now a string and not a Chatter instance. Run the playground now and you’ll see Charlotte’s new message printed

Let’s look at one more example. Add the following to the playground, and then run it, but before you do, pause and think about what will be written to the console.

james.message.value = "James: Doing great. You?"
charlotte.message.value = "Charlotte: I'm doing fine thanks."

You see both of the new messages, even though the current value of the chat publisher is “james”. This is the power of flatMap - both publishers have been combined into one output.

[Still on Slide 02]

Here’s an example of a flatMap diagram showing a special case of flatMap - limiting the number of publishers that the operator will accept. 3 publishers come in, but the operator deals only with 2, so only the publishers inside publishers 1 and 2 are processed, combined, and sent downstream.

OK, we’ve gone a little deeper into Transforming operators in this episode, taking a look at the somewhat more complex scan and flatMap operators: scan lets you build upon the most recent output from the operator; flatMap lets you combine values from multiple publishers into a single publisher to send to a downstream consumer. This is commonly seen with publishers that themselves emit publishers, but not always.

OK, we’re done with Transforming operators, so who’s up for a challenge? In the next episode we’ll put your skills to the test.