RxSwift: Transforming Operators in Practice
Learn how to work with transforming operators in RxSwift, in the context of a real app, in this tutorial taken from our latest book, RxSwift: Reactive Programming With Swift! By Marin Todorov.
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 in Practice
35 mins
- Getting Started With GitFeed
- Fetching Data From the Web
- Using map to Build a Request
- Using flatMap to Wait for a Web Response
- share vs. shareReplay
- Transforming the Response
- Processing the Response
- Intermission: Handling Erroneous Input
- Persisting Objects to Disk
- Add a Last-Modified Header to the Request
- Challenges
- Where to Go From Here?
Using flatMap to Wait for a Web Response
In the previous tutorial, you learned that flatMap
flattens out observable sequences. One of the common applications of flatMap
is to add some asynchronicity to a transformation chain. Let’s see how that works.
When you chain several transformations, that work happens synchronously. That is to say, all transformation operators immediately process each other’s output:
When you insert a flatMap
in between, you can achieve different effects:
-
You can flatten observables that instantly emit elements and complete, such as the
Observable
instances you create out of arrays of strings or numbers. - You can flatten observables that perform some asynchronous work and effectively “wait” for the observable to complete, and only then let the rest of the chain continue working.
What you need to do in your GitFeed code is something like this:
To do that, append the following code to the operator chain that you have so far:
.flatMap { request -> Observable<(HTTPURLResponse, Data)> in
return URLSession.shared.rx.response(request: request)
}
You use the RxCocoa response(request:)
method on the shared URLSession
object. That method returns an Observable
, which completes whenever your app receives the full response from the web server. You will learn more about the RxCocoa rx
extensions and how to extend Foundation and UIKit classes yourself in the full RxSwift book.
In the code you just wrote, flatMap
allows you to send the web request and receive a response without the need of protocols and delegates. How cool is that? Freely mixing map
and flatMap
transformations (as above) enables the kind of linear yet asynchronous code you hopefully are starting to appreciate.
Finally, to allow more subscriptions to the result of the web request, chain one last operator. You will use shareReplay(1)
to share the observable and keep in a buffer the last emitted event:
.shareReplay(1)
Here you’re using shareReplay(_)
. Let’s have a look why.
share vs. shareReplay
URLSession.rx.response(request:)
sends your request to the server and upon receiving the response emits once a .next
event with the returned data, and then completes.
In this situation, if the observable completes and then you subscribe to it again, that will create a new subscription and will fire another identical request to the server.
To prevent situations like this, you use shareReplay(_)
. This operator keeps a buffer of the last X
emitted elements and feeds them to any newly subscribed observer. Therefore if your request has completed and a new observer subscribes to the shared sequence (via shareReplay(_)
) it will immediately receive the response from the server that’s being kept in the buffer.
The rule of thumb for using shareReplay(_)
is to use it on any sequences you expect to complete – this way you prevent the observable from being re-created. You can also use this if you’d like observers to automatically receive the last X emitted events.
Transforming the Response
It will probably not come as a surprise that along with all the map
transforms you did before sending the web request, you will need to do some more after you receive its response.
If you think about it, the URLSession
class gives you back a Data
object, and this is not an object you can work with right away. You need to transform it to JSON and then to a native object you can safely use in your code.
You’ll now create a subscription to the response
observable that converts the response data into objects. Just after that last piece of code you wrote, add the following code on a new line:
response
.filter { response, _ in
return 200..<300 ~= response.statusCode
}
With the filter
operator above, you easily discard all error response codes. Your filter will only let through responses having a status code between 200
and 300
, which is all the success status codes.
~=
operator? It’s one of the lesser-known Swift operators, and when used with a range on its left side, checks if the range includes the value on its right side.
The data you receive will generally be a JSON-encoded server response containing a list of event objects. As your next task, you will try transforming the response data to an array of dictionaries.
Append another map
to the last operator chain:
.map { _, data -> [[String: Any]] in
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let result = jsonObject as? [[String: Any]] else {
return []
}
return result
}
Let’s deconstruct this piece of code:
- Unlike what you’ve done previously, you discard the response object and take only the response data.
-
You aid the compiler by letting it know you will return an
Array
. This is what an array of JSON objects looks like. -
You proceed to use
JSONSerialization
as usual to try to decode the response data and return the result. -
In case
JSONSerialization
fails, you return an empty array.
It’s really cool how RxSwift forces you to encapsulate these discrete pieces of work by using operators. And as an added benefit, you are always guaranteed to have the input and output types checked at compile time.
You are almost finished processing the API response. There’s a couple of things left to do before updating the UI. First, you need to filter out any responses that do not contain any event objects. Append to the chain:
.filter { objects in
return objects.count > 0
}
This will discard any error responses or any responses that do not contain new events since you last checked. You’ll implement fetching only new events later in the tutorial, but you can account for this now and help out your future self. :]
As a final transformation, you will convert the list of JSON objects to a collection of Event
objects. Open Event.swift from the starter project and you will see that the class already includes the following:
-
A handy
init
that takes a JSON object as a parameter -
A dynamic property named
dictionary
that exports the event as a JSON object
That’s about everything you need this data entity class to do.
Switch back to ActivityController.swift and append this to the last operator chain inside fetchEvents(repo:)
:
.map { objects in
return objects.map(Event.init)
}
This final map
transformation takes in a [[String: Any]]
parameter and outputs an [Event]
result. It does that by calling map
on the array itself and transforming its elements one-by-one.
Bam! map
just went meta! You’re doing a map
inside of a map
. :]
I hope you noticed the difference between the two maps. One is a method on an Observable
instance and is acting asynchronously on each emitted element. The second map
is a method on an Array
; this map
synchronously iterates over the array elements and converts them using Event.init
.
Finally, it’s time to wrap up this seemingly endless chain of transformations and get to updating the UI. To simplify the code, you will write the UI code in a separate method. For now, simply append this code to the final operator chain:
.subscribe(onNext: { [weak self] newEvents in
self?.processEvents(newEvents)
})
.addDisposableTo(bag)