(dotSwift) Presenting View Controllers 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.

I already posted a write up on the GitHub API search example here: http://rx-marin.com/post/dotswift-search-github-json-api/. Let’s continue with example number two.

Presenting a View Controller from RxSwift

RxSwift doesn’t neccessarily force you into one architecture or another, it’s really up to you to chose how are you going to structure your application. Same goes for navigation and how you move the user between your app’s screens and different view controllers.

Why I chose this example?

To make a point, namely that RxSwift plays very nicely with UIKit when neccessary, I chose to demo a simple code to present a view controller, get data back from that view controller, and navigate back to the presenting controller.

It’s a simple demo but demonstrates well one of the biggest benefits of using RxSwift - obliterating the need to deal with delegate to simply communicate between classes.

The sample code

In this post I can go in a bit more detail about the complete code than it was reasonable in the talk at the conference. Still keep in mind this was written with the goal to fit in a single slide, so corners were cut big time :) And so let’s get started.

The example is ultimately about an app featuring two view controllers:

  1. A list of GitHub repos (stored in an array), displayed in a table view.
  2. A modal view controller, which allows the user to add a new repo to the list.

Let’s first have a look at the list view controller. The repos are stored in a variable (for the purpose to simplify the example):

private let repos = Variable<[Repo]>(initialRepos)

And the initial values are just a list of Repo objects:

private let initialRepos = [
    Repo(1, "EasyAnimation", "Swift"),
    Repo(2, "Unbox", "Swift"),
    Repo(3, "RxSwift", "Swift")
]

repos are bound to the table view as usual via RxDataSources:

repos.asObservable()
  .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
  }
  .addDisposableTo(bag)

Next, to present a view controller with RxSwift, we’ll react to navigation item taps and, once pushed the view controller to the navigation stack, we’ll subscribe an observable on the presented controller. That observable will emit a next event and complete once the user has finished working with it.

We start with subscribing for taps:

navigationItem.rightBarButtonItem!.rx.tap
  .throttle(0.5, latest: false, scheduler: MainScheduler.instance)

Bu using throttle we’ll ignore any unintentional double taps (and avoid presenting more than one copy of the view controller one over each other).

Next we need to “kind of wait” until the observable of the view controller completes and get the data it emitted:

.flatMapFirst {[weak self] _ -> Observable<Repo> in
  if let addVC = self?.storyboard?.instantiateViewController(withIdentifier: "NewRepoViewController") 
  as? NewRepoViewController {
    self?.navigationController?.pushViewController(addVC, animated: true)
    return addVC.repoObservable
  }
  return Observable.never()
}

The block we provide to flatMap fetches the view controller from a storyboard and pushes it onto the navigation stack. Once presented, we return the repoObservable public property of the presented view controller.

That observable is going to give us back a Repo object in case the user has successfully created a new one. Once we have a Repo we can add it to the list and let the Rx binding from earlier update the table view.

We’ll subscribe the result and update the list:

.subscribe(onNext: {[weak self] repo in
  self?.repos.value.append(repo)
  _ = self?.navigationController?.popViewController(animated: true)
})

Since we know the observable is going to emit exactly one event, we also pop the view controller from the navigation stack. And that’s pretty much it! We present the controller, it gives us back data via a next event, and when it completes we discard it. Neat!

Now just for kicks have a look also at the code of the presented controller. The new repo controller exposes the user data via an observable like so:

private let repo = PublishSubject<Repo>()

lazy var repoObservable: Observable<Repo> = {
  return self.repo.asObservable()
}()

The controller itself can use the private repo subject to emit events, and other classes can subscribe to the repoObservable and react to events.

Let’s firstly combine all text values and make a Repo of them:

// current repo data
let currentRepo = Observable.combineLatest(
  id.rx.text, name.rx.text, language.rx.text) { id, name, lang -> Repo? in

  guard let id = id, let idInt = Int(id),
    let name = name, name.characters.count > 1,
    let lang = lang, lang.characters.count > 0 else {
      return nil
    }
    return Repo(idInt, name, lang)
  }
  .shareReplay(1)

We combine the sequence of values from the text fields id, name, and language and after a simple validation routine we emit a Repo object. The first subscription to currentRepo will update the UI:

// toggle save button
currentRepo
  .map { $0 != nil }
  .bindTo(saveButton.rx.isEnabled)
  .addDisposableTo(bag)

If the user’s input produces a valid Repo object, the save button is enabled so the user can add it to the list of existing repos. Next we’ll observe for taps on the Save bar item:

// emit repo when saved
saveButton.rx.tap
  .withLatestFrom(currentRepo)
  .subscribe(onNext: {[weak self] repo in
    if let repo = repo {
      self?.repo.onNext(repo)
      self?.repo.onCompleted()
    }
  })
  .addDisposableTo(bag)

Whenever the user taps the save button we grab the latest value of currentRepo and in the subscribe block, we emit it from the repo subjects. Any subscribers to repoObservable will get the event and dispose afterwards.

And there you go - with few lines of code in both view controllers you move away from the delegate pattern, which could be useful in some cases but your code ends up defining and implementing tons of protocols and as always it’s very hard to argue about the sequence in which methods are executed.

Through the use of the Observable class, you saw that the two view controllers can talk to each without the need of creating extra entities, protocols, etc.

The complete example

PresentViewController.swift

func bindUI() {
  // display data
  repos.asObservable()
    .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
    }
    .addDisposableTo(bag)

  // present view controller, observe output
  navigationItem.rightBarButtonItem!.rx.tap
    .throttle(0.5, latest: false, scheduler: MainScheduler.instance)
    .flatMapFirst {[weak self] _ -> Observable<Repo> in
      if let addVC = self?.storyboard?.instantiateViewController(withIdentifier: "NewRepoViewController") as? NewRepoViewController {
        self?.navigationController?.pushViewController(addVC, animated: true)
        return addVC.repoObservable
      }
      return Observable.never()
    }
    .subscribe(onNext: {[weak self] repo in
      self?.repos.value.append(repo)
      _ = self?.navigationController?.popViewController(animated: true)
    })
    .addDisposableTo(bag)
}

NewRepoViewController.swift

func bindUI() {
  // current repo data
  let currentRepo = Observable.combineLatest(id.rx.text, name.rx.text, language.rx.text) { id, name, lang -> Repo? in
    guard let id = id, let idInt = Int(id),
      let name = name, name.characters.count > 1,
      let lang = lang, lang.characters.count > 0 else {
        return nil
    }
    return Repo(idInt, name, lang)
    }
    .shareReplay(1)

  // toggle save button
  currentRepo
    .map { $0 != nil }
    .bindTo(saveButton.rx.isEnabled)
    .addDisposableTo(bag)

  // emit repo when saved
  saveButton.rx.tap
    .withLatestFrom(currentRepo)
    .subscribe(onNext: {[weak self] repo in
      if let repo = repo {
        self?.repo.onNext(repo)
        self?.repo.onCompleted()
      }
    })
    .addDisposableTo(bag)
}

Discussion

In real life you will probably never have the the whole presentation logic in one code chain like this. As said the idea above was to fit the code in a single slide and the audience to understand what’s happening without previous RxSwift knowledge.

But hey - it’s nice to know you can do that :) And once you know how the code works you can go about splitting the responsibilities of digging through the storyboard, presenting, and making use of the returned data into separate classes.

Hope that was an inspiration read! You can dig into more details below…

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.