(dotSwift) Search GitHub with RxSwift

In my talk at dotSwift 2017 I start with generic overview of some of the RxSwift basics and move to three complete code examples. In three posts I’ll post the sample code and comment shortly why I chose to highlight these exact examples. Let’s start with number one.

Searching for GitHub repositories with RxSwift

The first example in the talk is a simple single-screen application that allows the user to enter a repository name (or part of it) and search GitHub for matching results.

The completed application looks like this:

Why I chose this example?

One of the main points I make in my talk is that Rx and RxSwift in particular solves in big part the pains of asynchronous programming. Cocoa and the iOS SDK offers us a lot of different tools to battle asynchronos flows but there is no standard and we end up using many different (if not all) of these APIs. It’s not rare in complex app to use all of NotificationCenter, Grand Central Dispatch, closures, delegates, and more.

RxSwift solves this by offering a single standard protocol for asynchronous communication between any classes in the app - Observable. And this is what I wanted to demonstrate with the first example - how a rather diverse workflow that would usually be very complex, involving delegates and closures, becomes very simple to read, and sequential to write.

The sample code

The example starts with observing the text of a UITextField:

searchBar.rx.text

The issue with rx.text is that it emits String? since the field value is nil when there’s no text inside (Thanks, Obama). Luckily there’s an operator called orEmpty, which converts an optional nil to unwrapped default value. So for String? it returns "" (a non-optional empty string).

Chained to the previous code:

.orEmpty

This maps the observable to Observable<String>. Neat!

Next we want to filter search queries too short to be useful that will produce too many and rather irrelevant results. We chain:

.filter { query in
  return query.characters.count > 2
}

This will discard any searches for less than three characters. Next let’s discard any values emitted too fast, we don’t need to send all the network requests to GitHub’s server on each typed character if the user is typing fast. Chain to the previous code:

.debounce(0.5, scheduler: MainScheduler.instance)

If the user types and then stops for more than half a second, debounce will let through only the latest value before the user stopped typing. Ok it’s time to convert the search query into a web request:

.map { query in
  var apiUrl = URLComponents(string: "https://api.github.com/search/repositories")!
  apiUrl.queryItems = [URLQueryItem(name: "q", value: query)]
  return URLRequest(url: apiUrl.url!)
}

We build a URL and a URLRequest, which is ready to be sent to GitHub’s server. This map converts the observable to an Observable<URLRequest>.

Now by using the built-in reactive extension on URLSession we can get back the server response in JSON form:

.flatMapLatest { request in
  return URLSession.shared.rx.json(request: request)
    .catchErrorJustReturn([])
}

In case there was an error reaching out to the server, catchErrorJustReturn will make flatMapLatest return an empty array [] instead of erroring out. This converts the observable type to Observable<Any>. So we got the JSON … What next? Dig inside, find any returned repos and convert the data into objects:

.map { json -> [Repo] in
  guard let json = json as? [String: Any],
    let items = json["items"] as? [[String: Any]]  else {
      return []
  }
  return items.flatMap(Repo.init)
}

Using flatMap on the items collection will discard any objects that didn’t convert propertly to Repo objects. This final map converts the reponse to an Observable<[Repo]>.

We now have the desired outcome - a list of Repo objects. It’s time to show them in the view controller’s table view. Using RxCocoa’s bindTo binding the repos is a matter of few more lines:

.bindTo(tableView.rx.items) { tableView, row, repo in
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
    cell.textLabel!.text = repo.name
    cell.detailTextLabel?.text = repo.language
    return cell
}

The operator binds the list of Repo objects to the table’s rx.items. In the closure parameter you provide the code to deque and configure cells for the table.

If you want more advanced table binding (e.g. using animations, sections, and more) check out the RxDataSources library, which provides many different choices. If you’re working with Realm objects, there is a special library that allows you to use row animations and more automatically called RxRealmDataSources.

The complete example

searchBar.rx.text
  .orEmpty
  .filter { query in
    return query.characters.count > 2
  }
  .debounce(0.5, scheduler: MainScheduler.instance)
  .map { query in
    let apiUrl = URL(string: "https://api.github.com/search/repositories?q=" + query)!
    return URLRequest(url: apiUrl)
  }
  .flatMapLatest { request in
    return URLSession.shared.rx.json(request: request)
      .catchErrorJustReturn([])
  }
  .map { json -> [Repo] in
    guard let json = json as? [String: Any],
      let items = json["items"] as? [[String: Any]]  else {
        return []
    }
    return items.flatMap(Repo.init)
  }
  .bindTo(tableView.rx.items) { tableView, row, repo in
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
    cell.textLabel!.text = repo.name
    cell.detailTextLabel?.text = repo.language
    return cell
  }

Discussion

In real life you will probably never have the app’s logic layed out like this in one single line of code. (You might if you so desire; of course)

In a full-blown app you will have a networking layer, data layer, etc. You are likely to split this long chain of operators in two or three parts depending on what architecture you use.

In any case, following the Cocoa patterns you will have one delegate for the text field, one data source for the table, and URLSession will work with an asynchronous callback closure. The code is split into chunks, and you as the developer need to always keep in mind the sequence in which these methods will get called as to not corrupt the app state.

With RxSwift the code is easier to understand because it reads sequentially. Further it’s very easy to argue about the order in which code is executed even if it’s being executed asynchronously.

Links

The complete demo app from my talk: https://github.com/icanzilb/RxSwiftoniOS

The talk slides: https://speakerdeck.com/icanzilb/rxswift-on-ios

Hope that post was helpful, and if you want to get in touch you can find me here

Share this post:


If you'd like to learn how to create professional production apps with RxSwift, the best resource out there is the RxSwift book written by Florent Pillet, Junior Bontognali, Marin Todorov, & Scott Gardner.

It features 20+ chapters covering the basics, the Rx operators, and advanced topics like testing, error handling, and app architecture.

Available from Ray Wenderlich: » Learn more.
Tags// , , , ,