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.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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 call fetchEvents(repo:) from there.
  • In processEvents(), make sure you call tableView.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).