This tutorial has been taken from Chapter 8, “Transforming Operators in Practice” of our book RxSwift: Reactive Programming with Swift. The book covers everything from basic Rx theory, all the way up to error handling, UI, architecture, and other advanced Rx concepts. Enjoy!
In the previous tutorial on transforming operators, you learned about the real workhorses behind reactive programming with RxSwift: the map
and flatMap
dynamic duo.
Of course, those aren’t the only two operators you can use to transform observables, but a program can rarely do without using those two at least few times. The more experience you gain with these two, the better (and shorter) your code will be.
You already got to play around with transforming operators in the safety of a Swift playground, so hopefully you’re ready to take on a real-life project. You’ll get a starter project, which includes as much non-Rx code as possible, and you will complete that project by working through a series of tasks. In the process, you will learn more about map
and flatMap
, and in which situations you should use them in your code.
Without further ado, it’s time to get this show started!
Getting Started With GitFeed
Download the starter project for this tutorial here.
I wonder what the latest activity is on the RxSwift repository? In this tutorial, you’ll build a project to tell you this exact thing.
The project you are going to work on in this tutorial displays the activity of a GitHub repository, such as all the latest likes, forks, or comments. To get started with GitFeed, open the starter project for this tutorial, install the required CocoaPods, and open GitFeed.xcworkspace.
The app is a simple navigation controller project and features a single table view controller in which you will display the latest activity fetched from GitHub’s JSON API.
https://github.com/ReactiveX/RxSwift
, but if you’d like to change it to any other repository of your choice, feel free.
Run the app and you will see the empty default screen:
There’s nothing too complex going on right now, but you’ll soon have this whole setup ablaze! :]
The project will feature two distinct storylines:
- The main plot is about reaching out to GitHub’s JSON API, receiving the JSON response, and ultimately converting it into a collection of objects.
- The subplot is persisting the fetched objects to the disk and displaying them in the table before the “fresh” list of activity events is fetched from the server.
You will see that these two complement each other perfectly — and there are plenty of opportunities to use both map
and flatMap
to build what’s required.
Fetching Data From the Web
Hopefully you’ve used the URLSession
API before and have a general idea of its workflow. In summary: you create a URLRequest
containing a web URL and parameters, then send it off to the Internet. After a bit, you receive the server response.
With your current knowledge of RxSwift, it won’t be difficult to add a reactive extension to the URLSession
class. In this tutorial, you will simply use a solution boxed with RxCocoa — RxSwift’s companion library.
If you peek into GitFeed’s Podfile, you will notice that you import two different CocoaPods: RxSwift
and RxCocoa
. What gives?
RxCocoa is a library based on RxSwift, which implements many helpful APIs to aid with developing against RxSwift on Apple’s platforms. In an effort to keep RxSwift itself as close as possible to the common Rx API shared between all implementations such as RxJS, RxJava, and RxPython, all “extra functionality” is separated into RxCocoa.
You will use the default RxCocoa URLSession
extension to quickly fetch JSON from GitHub’s API in this tutorial.
Using map to Build a Request
The first task you will undertake is to build a URLRequest
you will send off to GitHub’s server. You will follow a reactive approach that might not make sense immediately, but don’t worry — when you re-visit that part of the project later on, you will appreciate it!
Open ActivityController.swift and peek inside. You configure the view controller’s UI in viewDidLoad()
, and when you’re finished, you call refresh()
. refresh()
in turn calls fetchEvents(repo:)
and hands over to it the repo name "ReactiveX/RxSwift"
.
It is in fetchEvents(repo:)
where you will add most of your code in this section. To get started, add the following:
let response = Observable.from([repo]) |
To start building the web request, you begin with a simple string, which is the repository’s full name. The idea to start with a string instead of directly building a URLRequest
is to be flexible with the observable’s input.
Next, take the address string and create the fully qualified URL
of the activity API endpoint:
.map { urlString -> URL in return URL(string: "https://api.github.com/repos/\(urlString)/events")! } |
You use a couple of shortcuts to create the full URL by using a hard-coded string and force unwrapping the result. You end up with the URL to access the latest events’ JSON.
Have you noticed that you specified the closure’s output type? Did you really have to do that? The obvious answer is no; usually you don’t need to explicitly spell out closure input and output types. You can usually leave it to the compiler to figure those out.
However, especially in code where you have several map
and/or flatMap
operators chained together, you might need to help the compiler out. It will sometimes get lost in figuring out the proper types, but you can aid it by at least spelling out the output types. If you see an error about mismatched or missing types, you can add more type information to your closures and it’ll probably fix the problem.
But enough about compiler woes — back to coding!
Now that you have a URL
, you can move on to transforming it into a complete request. Chain to the last operator:
.map { url -> URLRequest in return URLRequest(url: url) } |
Easy enough: you use map
to transform a URL
to a URLRequest
by using the provided web address.
Nice work! You’ve chained a couple of map
operators to create a more complex transformation:
Now it’s time to bring flatMap
into play and fetch some JSON.
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<(HTTPURLResponse, Data)>
, 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 shareReply(1)
to share the observable and keep in a buffer the last emitted event:
.shareReply(1) |
Here you’re using shareReply(_)
. Let’s have a look why.
share vs. shareReply
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 shareReply(_)
. 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 shareReply(_)
) it will immediately receive the response from the server that’s being kept in the buffer.
The rule of thumb for using shareReply(_)
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<[String: Any]>
. 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) |
Processing the Response
Yes, it’s finally time to perform some side effects. You started with a simple string, built a web request, sent it off to GitHub, and received an answer back. You transformed the response to JSON and then to native Swift objects. Now it’s time to show the user what you’ve been cooking up behind the scenes all this time.
Add this code anywhere in ActivityController
’s body:
func processEvents(_ newEvents: [Event]) { } |
In processEvents(_:)
, you grab the last 50 events from the repository’s event list and store the list into the Variable
property events
on your view controller. You’ll do that manually for now, since you haven’t yet learned how to directly bind sequences to variables or subjects.
Insert into processEvents()
:
var updatedEvents = newEvents + events.value if updatedEvents.count > 50 { updatedEvents = Array<Event>(updatedEvents.prefix(upTo: 50)) } events.value = updatedEvents |
You append the newly fetched events to the list in events.value
. Additionally, you cap the list to 50 objects. This way you will show only the latest activity in the table view.
Finally, you set the value of events
and are ready to update the UI. Since the data source code is already included in ActivityController
, you simply reload the table view to display the new data. To the end of the processEvents
function, add the following line:
tableView.reloadData() |
Run the app, and you should see the latest activity from GitHub. Yours will be different, depending on the current state of the repo in GitHub.
Since the code that came with the starter project in viewDidLoad()
sets up a table refresh control, you can try to pull down the table. As soon as you pull far enough, the refresh control calls the refresh()
method and reloads the events.
If someone forked or liked the repo since the last time you fetched the repo’s events, you will see new cells appear on top.
There is a little issue when you pull down the table view: the refresh control never disappears, even if your app has finished fetching data from the API. To hide it when you’ve finished fetching events, add the following code just below tableView.reloadData()
:
refreshControl?.endRefreshing() |
endRefreshing()
will hide the refresh control and reset the table view to its default state.
So far, you should have a good grasp of how and when to use map
and flatMap
. Throughout the rest of the tutorial, you are going to tie off a few loose ends of the GitFeed project to make it more complete.
Intermission: Handling Erroneous Input
The project as-is is pretty solid, at least in the perfect safety of a Swift Playground or in a step-by-step tutorial like this one. In this short intermission, you are going to look into some real-life server woes that your app might experience.
Switch to Event.swift and have a look at its init
. What would happen if one of those objects coming from the server contained a key with a wrong name? Yes you guessed it — your app would crash. The code of the Event
class is written somewhat lazily, and it assumes the server will always return valid JSON.
Fix this quickly before moving on. First of all, you need to change the init
to a failing initializer. Add a question mark right after the word init
like so:
init?(dictionary: AnyDict) |
This way, you can return nil
from the initializer instead of crashing the app. Find the line fatalError()
and replace it with the following:
return nil |
As soon as you do that, you will see a few errors pop up in Xcode. The compiler complains that your subscription in ActivityController
expects [Event]
, but receives an [Event?]
instead. Since some of the conversions from JSON to an Event
object might fail, the result has now changed type to [Event?]
.
Fear not! This is a perfect opportunity to exercise the difference between map
and flatMap
one more time. In ActivityController
, you are currently converting JSON objects to events via map(Event.init)
. The shortcoming of this approach is that you can’t filter out nil
elements and change the result, so to say, in mid-flight.
What you want to do is filter out any calls to Event.init
that returned nil
. Luckily, there’s a function that can do this for you: flatMap
— specifically, the flatMap
on Array
(not Observable
).
Return to ActivityController.swift
and scroll to fetchEvents(repo:)
. Replace .map(Event.init)
with:
objects.flatMap(Event.init) |
To recap: any Event.init
calls will return nil
, and flatMap
on those objects
will remove any nil
values, so you end up with an Observable
that returns an array of Event
objects (non-optional!). And since you removed the call to fatalError()
in the Event.init
function, your code is now safer. :]
Persisting Objects to Disk
In this section, you are going to work on the subplot as described in the introduction, where you will persist objects to disk, so when the user opens the app they will instantly see the events you last fetched.
In this example, you are about to persist the events to a .plist
file. The amount of objects you are about to store is small, so a .plist
file will suffice for now.
First, add a new property to the ActivityController
class:
private let eventsFileURL = cachedFileURL("events.plist") |
eventsFileURL
is the file URL where you will store the events file on your device’s disk. It’s time to implement the cachedFileURL
function to grab a URL to where you can read and write files. Add this outside the definition of the view controller class:
func cachedFileURL(_ fileName: String) -> URL { return FileManager.default .urls(for: .cachesDirectory, in: .allDomainsMask) .first! .appendingPathComponent(fileName) } |
Add that function anywhere in the controller file. Now, scroll down to processEvents(_:)
and append this to the bottom:
let eventsArray = updatedEvents.map{ $0.dictionary } as NSArray eventsArray.write(to: eventsFileURL, atomically: true) |
In this code, you convert updatedEvents
to JSON objects (a format also good for saving in a .plist
file) and store them in eventsArray
, which is an instance of NSArray
. Unlike a native Swift array, NSArray
features a very simple and straight-forward method to save its contents straight to a file.
To save the array, you call write(to:atomically:)
and give it the URL of the file where you want to create the file (or overwrite an existing one).
Cool! processEvents(_:)
is the place to perform side effects, so writing the events to disk in that place feels right. But where can you add the code to read the saved events from disk?
Since you need to read the objects back from the file just once, you can do that in viewDidLoad()
. This is where you will check if there’s a file with stored events, and if so, load its contents into events
.
Scroll up to viewDidLoad()
and add this just above the call to refresh()
:
let eventsArray = (NSArray(contentsOf: eventsFileURL) as? [[String: Any]]) ?? [] events.value = eventsArray.flatMap(Event.init) |
This code works similarly to the one you used to save the objects to disk — but in reverse. You first create an NSArray
by using init(contentsOf:)
, which tries to load list of objects from a plist
file and cast it as Array<[String: Any]>
.
Then you do a little dance by using flatMap
to convert the JSON to Event
objects and filter out any failing ones. Even though you persisted them to disk, they all should be valid, but hey — safety first! :]
That should do it. Delete the app from the Simulator, or from your device if you’re working there. Then run the app, wait until it displays the list of events, and then stop it from Xcode. Run the project a second time, and observe how the table view instantly displays the older data while the app fetches the latest events from the web.
Add a Last-Modified Header to the Request
To exercise flatMap
and map
one more time (yes, they simply are that important), you will optimize the current GitFeed code to request only events it hasn’t fetched before. This way, if nobody has forked or liked the repo you’re tracking, you will receive an empty response from the server and save on network traffic and processing power.
First, add a new property to ActivityController
to store the file name of the file in question:
private let modifiedFileURL = cachedFileURL("modified.txt") |
This time you don’t need a .plist
file, since you essentially need to store a single string like Mon, 30 May 2017 04:30:00 GMT
. This is the value of a header named Last-Modified
that the server sends alongside the JSON response. You need to send the same header back to the server with your next request. This way, you leave it to the server to figure out which events you last fetched and if there are any new ones since then.
As you did previously for the events list, you will use a Variable
to keep track of the Last-Modified
header. Add the following new property to ActivityController
:
fileprivate let lastModified = Variable<NSString?>(nil) |
You will work with an NSString
object for the same reasons you used an NSArray
before — NSString
can easily read and write to disk, thanks to a couple of handy methods.
Scroll to viewDidLoad()
and add this code above the call to refresh()
:
lastModified.value = try? NSString(contentsOf: modifiedFileURL, usedEncoding: nil) |
If you’ve previously stored the value of a Last-Modified
header to a file, NSString(contentsOf:usedEncoding:)
will create an NSString
with the text; otherwise, it will return a nil
value.
Start with filtering out the error responses. Move to fetchEvents()
and create a second subscription to the response
observable by appending the following code to the bottom of the method:
response .filter {response, _ in return 200..<400 ~= response.statusCode } |
Next you need to:
-
Filter all responses that do not include a
Last-Modified
header. - Grab the value of the header.
-
Convert it to an
NSString
value. - Finally, filter the sequence once more, taking the header value into consideration.
It does sound like a lot of work, and you might be planning on using a filter
, map
, another filter
, or more. In this section, you will use a single flatMap
to easily filter the sequence.
Consider the fact that flatMap
only emits the values of an observable when the observable completes. Therefore, if an observable does not complete, flatMap
will never emit any values. You’ll use that phenomenon to filter responses that don’t feature a Last-Modified
header.
Append this to the operator chain from above:
.flatMap { response, _ -> Observable<NSString> in guard let value = response.allHeaderFields["Last-Modified"] as? NSString else { return Observable.never() } return Observable.just(value) } |
You use guard
to check if the response contains an HTTP header by the name of Last-Modified
, whose value can be cast to an NSString
. If you can make the cast, you return an Observable
with a single element; otherwise, you return an Observable
, which never emits any elements:
Now that you have the final value of the desired header, you can proceed to update the lastModified
property and store the value to the disk. Add the following:
.subscribe(onNext: { [weak self] modifiedHeader in guard let strongSelf = self else { return } strongSelf.lastModified.value = modifiedHeader try? modifiedHeader.write(to: strongSelf.modifiedFileURL, atomically: true, encoding: String.Encoding.utf8.rawValue) }) .addDisposableTo(bag) |
In your subscription’s onNext
closure, you update lastModified.value
with the latest date and then call NSString.write(to:atomically:encoding)
to save to disk. In the end, you add the subscription to the view controller’s dispose bag.
To finish working through this part of the app, you need to use the stored header value in your request to GitHub’s API. Scroll toward the top of fetchEvents(repo:)
and find the particular map
below where you create a URLRequest
:
.map { url -> URLRequest in return URLRequest(url: url) } |
Replace the above code with this:
.map { [weak self] url -> URLRequest in var request = URLRequest(url: url) if let modifiedHeader = self?.lastModified.value { request.addValue(modifiedHeader as String, forHTTPHeaderField: "Last-Modified") } return request } |
In this new piece of code, you create a URLRequest
just as you did before, but you add an extra condition: if lastModified
contains a value, no matter whether it’s loaded from a file or stored after fetching JSON, add that value as a Last-Modified
header to the request.
This extra header tells GitHub that you aren’t interested in any events older than the header date. This will not only save you traffic, but responses which don’t return any data won’t count towards your GitHub API usage limit. Everybody wins!
In this tutorial, you learned about different real-life use cases for map
and flatMap
— and built a cool project along the way (even though you still need to handle the results on the main thread like the smart programmer you are).
Where to Go From Here?
You can download the final package from this tutorial here.
If you enjoyed what you learned in this tutorial, why not check out the complete RxSwift book, available on our store?
Here’s a taste of what’s in the book:
- Getting Started: Get an introduction to the reactive programming paradigm, learn the terminology involved and see how to begin using RxSwift in your projects.
- Event Management: Learn how to handle asynchronous event sequences via two key concepts in Rx — Observables and Observers.
- Being Selective: See how to work with various events using concepts such as filtering, transforming, combining, and time operators.
- UI Development: RxSwift makes it easy to work with the UI of your apps using RxCocoa, which provides an integration of both UIKit and Cocoa.
- Intermediate Topics: Level up your RxSwift knowledge with chapters on reactive networking, multi-threading, and error handling.
- Advanced Topics: Round out your RxSwift education by learning about MVVM app architecture, scene-based navigation, and exposing data via services.
- And much, much more!
By the end of this book, you’ll have hands-on experience solving common issues in a reactive paradigm — and you’ll be well on your way to coming up with your own Rx patterns and solutions!
To celebrate the launch of the book, it’s currently on sale for $44.99 – that’s a $10 discount off the cover price! But don’t wait too long, as this deal is only on until Friday, April 7.
If you have any questions or comments on this tutorial, feel free to join the discussion below!
The post RxSwift: Transforming Operators in Practice appeared first on Ray Wenderlich.