7.
Transforming Operators
Written by Scott Gardner
Before you decided to buy this book and commit to learning RxSwift, you might have felt that RxSwift was some esoteric library; elusive, yet strangely compelling you to master it. And maybe that reminds you of when you first started learning iOS or Swift.
Now that you’re up to Chapter 7, you’ve come to realize that RxSwift isn’t magic. It’s a carefully constructed API that does a lot of heavy lifting for you and streamlines your code. You should be feeling good about what you’ve learned so far.
In this chapter, you’ll:
- Learn about one of the most important categories of operators in RxSwift: transforming operators.
- 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(_:)andflatMap(_:).
By the end of this chapter, you’ll be transforming all the things!
Getting started
Run ./bootstrap.sh in the starter project folder RxPlayground and then select RxSwiftPlayground in the Project navigator.
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 shown in this marble diagram, toArray will convert an observable sequence of elements into an array of those elements once the observable completes. The toArray operator returns a Single. Recall from Chapter 2, “Observables,” that Single is a trait that emits either a success event containing the value, or an error event containing the error. In this case, it will emit a success event containing the 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(onSuccess: {
print($0)
})
.disposed(by: disposeBag)
}
With this code, you:
- Create a finite observable of letters.
- Use
toArrayto transform the individual elements into an array.
You’ll notice the code above prints out the following:
--- 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<Int>.of(123, 4, 56)
// 3
.map {
formatter.string(for: $0) ?? ""
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
Here’s the play-by-play:
- You create a number formatter to spell out each number.
- You create an observable of
Int. - 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.
In Chapter 5, “Filtering Operators,” you learned about using enumerated and map with filtering operators. You’ll run through another example of using enumerated with map next. Add this code to your playground:
example(of: "enumerated and map") {
let disposeBag = DisposeBag()
// 1
Observable.of(1, 2, 3, 4, 5, 6)
// 2
.enumerated()
// 3
.map { index, integer in
index > 2 ? integer * 2 : integer
}
// 4
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
Step by step, you:
- Create an observable of integers.
- Use
enumeratedto produce tuple pairs of each element and its index. - Use
map, and destructure the tuple into individual arguments. If the element’sindexis greater than2, multiply it by2and return it; else, return it as-is. - Subscribe and print elements as they’re emitted.
Only the fourth element onward will be transformed and sent to the subscriber to be printed.
--- Example of: enumerated and map ---
1
2
3
8
10
12
You’ve also learned about the filter operator. The compactMap operator is a combination of the map and filter operators that specifically filters out nil values, similarly to its counterpart in the Swift standard library. Add this example to your playground to see it in action:
example(of: "compactMap") {
let disposeBag = DisposeBag()
// 1
Observable.of("To", "be", nil, "or", "not", "to", "be", nil)
// 2
.compactMap { $0 }
// 3
.toArray()
// 4
.map { $0.joined(separator: " ") }
// 5
.subscribe(onSuccess: {
print($0)
})
.disposed(by: disposeBag)
}
With this code, you:
- Create an observable of
String?, which theofoperator infers from the values. - Use the
compactMapoperator to retrieve unwrapped value, and filter outnils. - Use
toArrayto convert the observable into aSinglethat emits an array of all its values. - Use
mapto join the values together, separated by a space. - Print the result in the subscription.
The final string, compact yet thought-provoking, is printed:
--- Example of: compactMap ---
To be or not to be
Up until this point, you’ve worked with observables of regular values. 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 {
let score: BehaviorSubject<Int>
}
Student is structure that has a score property that is a BehaviorSubject<Int>. 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. 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 says: “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 on the top line all the way through to the target observable on the bottom line that will deliver elements to the subscriber.
The source observable is an object type with 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 project its value property onto a new observable created just for O1 on the 1st line below flatMap. That observable is then flattened down to the target observable on the bottom line.
Later in time, O1’s value property changes to 4. This is not visually represented in the marble diagram. However, evidence that O1’s value has changed is that it is projected onto the existing observable for O1, and then flattened down to the target observable.
The next value in the source observable, O2, is received by flatMap. Its initial value 2 is projected onto a new observable for O2, and then it’s flattened down to the target observable. Later, O2’s value is changed to 5. That value is then projected and flattened to the target observable.
Finally, O3 is received by flatMap, its initial value of 3 is projected and flattened.
To recap, 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 laura = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 90))
// 2
let student = PublishSubject<Student>()
// 3
student
.flatMap {
$0.score
}
// 4
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
Here’s the play-by-play:
- You create two instances of
Student,lauraandcharlotte. - You create a source subject of type
Student. - You use
flatMapto reach into thestudentsubject and project itsscore. - You print out
nextevent elements in the subscription.
Nothing is printed yet. Add this code to the example:
student.onNext(laura)
As a result, laura’s score is printed out:
--- Example of: flatMap ---
80
Now change laura’s score by adding this code:
laura.score.onNext(85)
laura’s new score is printed:
85
Next, add a different Student instance onto the source subject by adding this code:
student.onNext(charlotte)
flatMap does its job and charlotte’s score is printed:
90
Here’s where it gets interesting. Change laura’s score by adding this line of code:
laura.score.onNext(95)
laura’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.onNext(100)
Sure enough, her new score is printed out:
100
To recap, flatMap keeps projecting changes from each observable. However, when you only want to keep up with the latest element in the source observable, use the flatMapLatest operator.
The flatMapLatest operator is actually a combination of two operators: map and switchLatest. You’ll learn about switchLatest in the next chapter, “Combining Operators,” but you’re getting a sneak peek here. switchLatest will produce values from the most recent observable, and unsubscribe from the previous observable.
Here’s the documentation for flatMapLatest: “Projects each element of an observable sequence into a new sequence of observable sequences and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.” That’s a lot to take in, however you’ve already learned flatMap and this one’s not much different. Check out the marble diagram of flatMapLatest.
flatMapLatest works just like flatMap to reach into an observable element to access its observable property and project it onto a new sequence for each element of the source observable. Those elements are flattened down into a target observable that will provide elements to the subscriber. What makes flatMapLatest different is that it will automatically switch to the latest observable and unsubscribe from the previous one.
In the previous marble diagram, O1 is received by flatMapLatest. It projects its value onto a new observable for O1, and flattens it down to the target observable. Just like before. But then flatMapLatest receives O2 and switches to O2 because it’s now the latest.
The process repeats when O3 is received by flatMapLatest. It then switches to its sequence and ignores the previous one (O2). The result is that the target observable only receives elements from the latest observable.
Add the following example to your playground, which is a duplicate of the previous example, except for changing flatMap to flatMapLatest:
example(of: "flatMapLatest") {
let disposeBag = DisposeBag()
let laura = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 90))
let student = PublishSubject<Student>()
student
.flatMapLatest {
$0.score
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
student.onNext(laura)
laura.score.onNext(85)
student.onNext(charlotte)
// 1
laura.score.onNext(95)
charlotte.score.onNext(100)
}
Only one thing to point out here that’s different from the previous example of flatMap:
- Changing
laura’s score here will have no effect. It will not be printed out. This is becauseflatMapLatestswitched to the latest observable, forcharlotte:
--- Example of: flatMapLatest ---
80
85
90
100
Are you wondering when would you use flatMap for flatMapLatest? One of the most common use cases for flatMapLatest is with networking operations, which you’ll do later in the book. Imagine that you’re implementing type-ahead search. As the user types each letter, s, w, i, f, t, you want to execute a new search and ignore results from the previous one. flatMapLatest is how you do that.
Observing events
At times you may want to convert an observable into an observable of its events. One typical scenario where this is useful is when you do not have control over an observable that has observable properties, and you want to handle error events to avoid terminating outer sequences.
Enter this new example into the playground:
example(of: "materialize and dematerialize") {
// 1
enum MyError: Error {
case anError
}
let disposeBag = DisposeBag()
// 2
let laura = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 100))
let student = BehaviorSubject(value: laura)
}
Setting up this example, you:
- Create an error type.
- Create two instances of
Studentand a student behavior subject with the first studentlauraas its initial value.
Similar to the previous two examples, you want to subscribe to the inner score property of Student. Add this code to the example:
// 1
let studentScore = student
.flatMapLatest {
$0.score
}
// 2
studentScore
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
// 3
laura.score.onNext(85)
laura.score.onError(MyError.anError)
laura.score.onNext(90)
// 4
student.onNext(charlotte)
Continuing this example, you:
- Create a
studentScoreobservable usingflatMapLatestto reach into thestudentobservable and access itsscoreobservable property. - Subscribe to and print out each
scorewhen it is emitted. - Add a score, error, and another score onto the current student.
- Add the second student
charlotteonto thestudentobservable. Because you usedflatMapLatest, this will switch to this new student and subscribe to herscore.
This error is unhandled. The studentScore observable terminates, and so does the outer student observable:
--- Example of: materialize and dematerialize ---
80
85
Unhandled error happened: anError
subscription called from:
Using the materialize operator, you can wrap each event emitted by an observable in an observable.
Change the studentScore implementation to the following:
let studentScore = student
.flatMapLatest {
$0.score.materialize()
}
Option-click on studentScore and you’ll see it is now an Observable<Event<Int>>. And the subscription to it now emits events. The error still causes the studentScore to terminate, but not the outer student observable, so when you switch to the new student, its score is successfully received and printed:
--- Example of: materialize and dematerialize ---
next(80)
next(85)
error(anError)
next(100)
However, now you’re dealing with events, not elements. That’s where dematerialize comes in. It will convert a materialized observable back into its original form.
Change the subscription to the following:
studentScore
// 1
.filter {
guard $0.error == nil else {
print($0.error!)
return false
}
return true
}
// 2
.dematerialize()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
Wrapping this example up, you:
- Print and filter out any errors.
- Use
dematerializeto return thestudentScoreobservable to its original form, emitting scores and stop events, not events of scores and stop events.
Now your student observable is protected by errors on its inner score observable. The error is printed and laura’s studentScore is terminated, so adding a new score onto her does nothing. But when you add charlotte onto the student subject, her score is printed:
--- Example of: materialize and dematerialize ---
80
85
anError
100
Challenge
Completing challenges helps drive home what you learned in the chapter. There are starter and finished versions of the challenge in the exercise files download.
Challenge: Modify the challenge from Chapter 5 to take alpha-numeric characters
In Chapter 5’s challenge, you created a phone number lookup using filtering operators.
You added the code necessary to look up a contact based on a 10-digit number entered by the user.
input
.skipWhile { $0 == 0 }
.filter { $0 < 10 }
.take(10)
.toArray()
.subscribe(onNext: {
let phone = phoneNumber(from: $0)
if let contact = contacts[phone] {
print("Dialing \(contact) (\(phone))...")
} else {
print("Contact not found")
}
})
.disposed(by: disposeBag)
Your goal for this challenge is to modify this implementation to be able to take letters as well, and convert them to their corresponding number based on a standard phone keypad (abc is 2, def is 3, and so on).
The starter project includes a helper closure to do the conversion:
let convert: (String) -> Int? = { value in
if let number = Int(value),
number < 10 {
return number
}
let convert: [String: Int] = [
"abc": 2, "def": 3, "ghi": 4,
"jkl": 5, "mno": 6, "pqrs": 7,
"tuv": 8, "wxyz": 9
]
let converted = keyMap
.filter { $0.key.contains(value.lowercased()) }
.map(\.value)
.first
return converted
}
And there are closures to format and “dial” the contact if found — actually, just print it out:
let format: ([Int]) -> String = {
var phone = $0.map(String.init).joined()
phone.insert("-", at: phone.index(
phone.startIndex,
offsetBy: 3)
)
phone.insert("-", at: phone.index(
phone.startIndex,
offsetBy: 7)
)
return phone
}
let dial: (String) -> String = {
if let contact = contacts[$0] {
return "Dialing \(contact) (\($0))..."
} else {
return "Contact not found"
}
}
These closures allow you to move the logic out of the subscription, where it really doesn’t belong. So what’s left to do then?
- Use multiple
maps to perform each transformation along the way. - Use
skipWhilejust like you did in Chapter 5 to skip0s at the beginning. - Handle the optionals returned from
convert.
To handle the optionals, you can use a handy operator excerpted from the RxSwiftExt repo: unwrap. RxSwiftExt includes useful operators that are not part of the core RxSwift library. The unwrap operator replaces the need to do this:
Observable.of(1, 2, nil, 3)
.filter { $0 != nil }
.map { $0! }
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
With unwrap, you can just do this:
Observable.of(1, 2, nil, 3)
.unwrap()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
If you think that’s cool, you’re in luck, because there’s a whole chapter dedicated to RxSwiftExt later.
The starter project also includes code to test your solution. Just add your solution right below the comment // Add your code here.