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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
RxSwift: Transforming Operators
20 mins
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:
- Create an observable of letters.
-
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:
- You create a number formatter to spell out each number.
-
You create an observable of
NSNumber
s (so that you don’t have to convert integers when using the formatter next). -
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 returnsnil
.
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:
- You create an observable of integers.
-
You use
mapWithIndex
, and if the element’sindex
is greater than2
, multiply it by2
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.
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:
-
You create two instances of
Student
,ryan
andcharlotte
. -
You create a source subject of type
Student
. -
You use
flatMap
to reach into thestudent
subject and access itsscore
, which is aVariable
, so you callasObservable()
on it. You don’t modifyscore
in any way. Just pass it through. -
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?