RxSwift: Transforming Operators

Learn how to leverage transforming operators in RxSwift, in this tutorial taken from our latest book, RxSwift: Reactive Programming With Swift! By Scott Gardner.

Leave a rating/review
Save for later
Share

In this tutorial, you’re going to learn about one of the most important categories of operators in RxSwift: transforming operators. You’ll use transforming operators all the time, to prep data coming from an observable for use by your subscriber.

Once again, there are parallels between transforming operators in RxSwift and the Swift standard library, such as map(_:) and flatMap(_:). By the end of this tutorial, you’ll be transforming all the things!

Getting Started

The starter project for this tutorial is named RxSwiftPlayground; you can download it here. Once you’ve opened it and done an initial build, you’re ready for action.

Transforming Elements

Observables emit elements individually, but you will frequently want to work with collections, such as when you’re binding an observable to a table or collection view, which you’ll learn how to do later in the book. A convenient way to transform an observable of individual elements into an array of all those elements is by using toArray. As depicted in this marble diagram, toArray will convert an observable sequence of elements into an array of those elements, and emit a .next event containing that array to subscribers.

Add this new example to your playground:

example(of: "toArray") {

  let disposeBag = DisposeBag()

  // 1
  Observable.of("A", "B", "C")
    // 2
    .toArray()
    .subscribe(onNext: {
      print($0)
    })
    .addDisposableTo(disposeBag)
}

Here’s what you just did:

  1. Create an observable of letters.
  2. Use toArray to transform the elements in an array.

An array of the letters is printed.

--- Example of: toArray ---
["A", "B", "C"]

RxSwift’s map operator works just like Swift’s standard map, except it operates on observables. In the marble diagram, map takes a closure that multiplies each element by 2.

Add this new example to your playground:

example(of: "map") {

  let disposeBag = DisposeBag()

  // 1
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut

  // 2
  Observable<NSNumber>.of(123, 4, 56)
    // 3
    .map {
      formatter.string(from: $0) ?? ""
    }
    .subscribe(onNext: {
      print($0)
    })
    .addDisposableTo(disposeBag)
}

Here’s the play-by-play:

  1. You create a number formatter to spell out each number.
  2. You create an observable of NSNumbers (so that you don’t have to convert integers when using the formatter next).
  3. You use map, passing a closure that gets and returns the result of using the formatter to return the number’s spelled out string or an empty string if that operation returns nil.

Chapter 5 of the book covers filtering operators, some of them with withIndex variations. The same holds true for transforming operators. mapWithIndex also passes the element’s index to its closure. In this marble diagram, mapWithIndex will transform the element by multiplying it by 2 if its index is greater than 1, otherwise it will pass through the element as-is, so only the 3rd element is transformed.

Now add this new example to your playground to implement the example in the marble diagram:

example(of: "mapWithIndex") {

  let disposeBag = DisposeBag()

  // 1
  Observable.of(1, 2, 3, 4, 5, 6)
    // 2
    .mapWithIndex { integer, index in
      index > 2 ? integer * 2 : integer
    }
    .subscribe(onNext: {
      print($0)
    })
    .addDisposableTo(disposeBag)
}

Quite simply:

  1. You create an observable of integers.
  2. You use mapWithIndex, and if the element’s index is greater than 2, multiply it by 2 and return it, else return it as is.

Only the fourth element onward will be transformed and sent to the subscriber to be printed.

--- Example of: mapWithIndex ---
1
2
3
8
10
12

You may have wondered at some point, “How do I work with observables that are properties of observables?” Enter the matrix.

Transforming Inner Observables

Add the following code to your playground, which you’ll use in the upcoming examples:

struct Student {
    
  var score: Variable<Int>
}

Student is structure that has a score property that is a Variable. RxSwift includes a few operators in the flatMap family that allow you to reach into an observable and work with its observable properties. You’re going to learn how to use the two most common ones here.

Note: A heads up before you begin: these operators have elicited more than their fair share of questions (and groans and moans) from newcomers to RxSwift. They may seem complex at first, but you are going to walk through detailed explanations of each, so by the end of section you’ll be ready to put these operators into action with confidence.

The first one you’ll learn about is flatMap. The documentation for flatMap describes that it “Projects each element of an observable sequence to an observable sequence and merges the resulting observable sequences into one observable sequence.” Whoa! That description, and the following marble diagram, may feel a bit overwhelming at first. Read through the play-by-play explanation that follows, referring back to the marble diagram.

The easiest way to follow what’s happening in this marble diagram is to take each path from the source observable (the top line) all the way through to the target observable that will deliver elements to the subscriber (the bottom line). The source observable is of an object type that has a value property that itself is an observable of type Int. It’s value property’s initial value is the number of the object, that is, O1’s initial value is 1, O2’s is 2, and O3’s is 3.

Starting with O1, flatMap receives the object and reaches in to access its value property and multiply it by 10. It then projects the transformed elements from O1 onto a new observable (the 1st line below flatMap just for O1), and that observable is flattened down to the target observable that will deliver elements to the subscriber (the bottom line).

Later, O1’s value property changes to 4, which is not visually represented in the marble diagram (otherwise the diagram would become even more congested). But the evidence that O1’s value has changed is that it is transformed, projected onto the existing observable for O1 as 40, and then flattened down to the target observable. This all happens in a time-linear fashion.

The next value in the source observable, O2, is received by flatMap, its initial value 2 is transformed to 20, projected onto a new observable for O2, and then flattened down to the target observable. Later, O2’s value is changed to 5. It is transformed to 50, projected, and flattened to the target observable.

Finally, O3 is received by flatMap, its initial value of 3 is transformed, projected, and flattened.

flatMap projects and transforms an observable value of an observable, and then flattens it down to a target observable. Time to go hands-on with flatMap and really see how to use it. Add this example to your playground:

example(of: "flatMap") {

  let disposeBag = DisposeBag()

  // 1
  let ryan = Student(score: Variable(80))
  let charlotte = Student(score: Variable(90))

  // 2
  let student = PublishSubject<Student>()

  // 3
  student.asObservable()
    .flatMap {
      $0.score.asObservable()
    }
    // 4
    .subscribe(onNext: {
      print($0)
    })
    .addDisposableTo(disposeBag)
}

Here’s the play-by-play:

  1. You create two instances of Student, ryan and charlotte.
  2. You create a source subject of type Student.
  3. You use flatMap to reach into the student subject and access its score, which is a Variable, so you call asObservable() on it. You don’t modify score in any way. Just pass it through.
  4. You print out .next event elements in the subscription.

Nothing is printed yet. Add this code to the example:

student.onNext(ryan)

As a result, ryan’s score is printed out.

--- Example of: flatMap ---
80

Now change ryan’s score by adding this code to the example:

ryan.score.value = 85

ryan’s new score is printed.

85

Next, add a different Student instance (charlotte) onto the source subject by adding this code:

student.onNext(charlotte)

flatMap does its thing and charlotte’s score is printed.

90

Here’s where it gets interesting. Change ryan’s score by adding this line of code:

ryan.score.value = 95

ryan’s new score is printed.

95

This is because flatMap keeps up with each and every observable it creates, one for each element added onto the source observable. Now change charlotte’s score by adding the following code, just to verify that both observables are being monitored and changes projected:

charlotte.score.value = 100

Sure enough, her new score is printed out.

100

To recap, flatMap keeps projecting changes from each observable. There will be times when you want this behavior. And there will be times when you only want to keep up with the latest element in the source observable. So what do you think is the name of the flatMap operator that only keeps up with the latest element?