Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

8. Transforming Operators in Practice
Written by Marin Todorov

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, 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. Like in other “… in practice” chapters, you will 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.

Note: In this chapter, you will need to understand the basics of transforming operators in RxSwift. If you haven’t worked through Chapter 7, “Transforming Operators”, do that first and then come back to this chapter.

Without further ado, it’s time to get this show started!

Getting started with GitFeed

I wonder what the latest activity is on the RxSwift repository? In this chapter, you’ll build a project to tell you this exact thing.

The project you are going to work on in this chapter 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 chapter, install the required CocoaPods (as explained in Chapter 1, “Hello RxSwift”), 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.

Note: The starter project is set to display the activity of 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 disk and displaying them in a 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.

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!

let response = Observable.from([repo])
.map { urlString -> URL in
  return URL(string: "https://api.github.com/repos/\(urlString)/events")!
}
.map { url -> URLRequest in
  return URLRequest(url: url)
}

Using flatMap to wait for a web response

In the previous chapter, 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.

.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
  return URLSession.shared.rx.response(request: request)
}
.share(replay: 1)

share() vs. share(replay: 1)

URLSession.rx.response(request:) sends your request to the server, and upon receiving the response, emits a .next event just once with the returned data, and then completes.

Transforming the response

It will probably not come as a surprise that along with all map transformations you did before sending the web request, you will need to do some more after you receive its response.

response
  .filter { response, _ in
    return 200..<300 ~= response.statusCode
  }
.compactMap { _, data -> [Event]? in
  return try? JSONDecoder().decode([Event].self, from: data)
}

.subscribe(onNext: { [weak self] newEvents in
  self?.processEvents(newEvents)
})
.disposed(by: 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.

var updatedEvents = newEvents + events.value
if updatedEvents.count > 50 {
  updatedEvents = [Event](updatedEvents.prefix(upTo: 50))
}

events.accept(updatedEvents)
tableView.reloadData()

DispatchQueue.main.async {
  self.tableView.reloadData()
}

self.refreshControl?.endRefreshing()

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.

private let eventsFileURL = cachedFileURL("events.json")
func cachedFileURL(_ fileName: String) -> URL {
  return FileManager.default
    .urls(for: .cachesDirectory, in: .allDomainsMask)
    .first!
    .appendingPathComponent(fileName)
}
let encoder = JSONEncoder()
if let eventsData = try? encoder.encode(updatedEvents) {
  try? eventsData.write(to: eventsFileURL, options: .atomicWrite)
}
let decoder = JSONDecoder()
if let eventsData = try? Data(contentsOf: eventsFileURL),
   let persistedEvents = try? decoder.decode([Event].self, from: eventsData) {
  events.accept(persistedEvents)
}

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.

private let modifiedFileURL = cachedFileURL("modified.txt")

private let lastModified = BehaviorRelay<String?>(value: nil)
if let lastModifiedString = try? String(contentsOf: modifiedFileURL, encoding: .utf8) {
  lastModified.accept(lastModifiedString)
}
response
  .filter { response, _ in
    return 200..<400 ~= response.statusCode
  }
.flatMap { response, _ -> Observable<String> in
  guard let value = response.allHeaderFields["Last-Modified"] as? String else {
    return Observable.empty()
  }
  return Observable.just(value)
}

.subscribe(onNext: { [weak self] modifiedHeader in
  guard let self = self else { return }

  self.lastModified.accept(modifiedHeader)
  try? modifiedHeader.write(to: self.modifiedFileURL, atomically: true, encoding: .utf8)
})
.disposed(by: bag)
.map { url -> URLRequest in
  return URLRequest(url: url)
}
.map { [weak self] url -> URLRequest in
  var request = URLRequest(url: url)
  if let modifiedHeader = self?.lastModified.value {
    request.addValue(modifiedHeader,
      forHTTPHeaderField: "Last-Modified")
  }
  return request
}

Challenge

Challenge: Fetch top repos and spice up the feed

In this challenge, you will go through one more map/flatMap exercise. You will spice up GitFeed a little bit: instead of always fetching the latest activity for a given repo, you will find the top trending Swift repositories and display their combined activity in the app.

let response = Observable.from(["https://api.github.com/search/repositories?q=language:swift&per_page=5"])
let response = Observable.from(["https://api.github.com/search/repositories?q=language:swift&per_page=5"])

[map to convert to to URLRequest]

[flatMap to fetch JSON back]

[flatMap to convert JSON to list of repo names,
  and create Observable from that list]

[existing code follows below]

.map { urlString -> URL in
  return URL(string: "https://api.github.com/repos/\(urlString)/events?per_page=5")!
}
.map { [weak self] url -> URLRequest in
  var request = URLRequest(url: url)
  ...
}

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now