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?
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.
You can use flatMap
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!
Challenges
Your challenge in this tutorial is to fix the fact that you're updating the UI from a background thread (and by this going against everything that UIKit stands for).
You will learn more about RxSwift schedulers and multi- threading in Chapter 15 of RxSwift: Reactive programming with Swift, “Intro to Schedulers / Threading in Practice.” In this simple tutorial though, you can work through a simple solution to the problem by using the DispatchQueue
type.
First of all, make sure you know what thread you’re running on by adding some test print statements. Scroll to fetchEvents(repo:)
, and inside the first flatMap
closure, insert print("main: \(Thread.isMainThread)")
so it looks like this:
.flatMap { request -> Observable<(HTTPURLResponse, Data)> in
print("main: \(Thread.isMainThread)")
return URLSession.shared.rx.response(request: request)
}
Then add the same print line in the filter immediately below that flatMap. Finally, scroll down and insert the same debug print line anywhere inside processEvents(_:)
. Run the app and have a look at Xcode’s console. You should be seeing something like this:
main: true
main: false
main: false
UIKit calls viewDidLoad()
on the main thread, so when you invoke fetchEvents(repo:)
all the code runs on the main thread too. This is also confirmed by the first output line main: true
.
But the second and third prints seem to have switched to a background thread. You can skim the code and reassure yourself you never switch threads manually.
Luckily, you only need to touch the current code in two places:
- In
refresh()
, switch to a background thread and callfetchEvents(repo:)
from there. - In
processEvents()
, make sure you calltableView.reloadData()
on the main thread.
That’s it! In case you need some assistance with writing the Grand Central Dispatch code to manage threads, consult the completed project provided with this chapter.
Of course if you want to learn how to do thread switching the Rx way, read more about schedulers and multi-threading in Chapter 15 of RxSwift: Reactive programming with Swift, “Intro to Schedulers / Threading in Practice.”
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).