Make UITableView more Reactive with RxSwift

If, like me, you are bored to always write the same kind of code to make simple things with the UITableView (and UICollectionView), this post is for you!

Thanks to RxSwift you can implement simple (and more complex) tableview behaviours with very few line of code.

What is RxSwift?

This post is not intended to explain you what is the reactive programming paradigm but to understand the following post you need to know some fundamentals.

To make it simple (and sorry for the shortcuts) the RxSwift framework is an implementation of the ReactiveX or RX which provides a unified api to work with Observables. An observable is an abstraction of streams of asynchronous events. It can be arrays, touch events, text update, and many more. Moreover it allows you to chain, filter, transform them to have more specific observables.

If you want further reading I suggest you this great introduction, this tutorial or the book Introduction to RX.

Let’s get started!

Before to start, you can find the source on github.

Now we are going to build a simple multiple-choice quiz using an UITableView object. The cells are used to display the choices, an UINavigationBar to display the question title, and a button in the bottom to submit our answers.

The model

We are now going to define our simple question model:

struct ChoiceModel {
  let title: String
  let valid: Bool
}

struct QuestionModel {
  let title: String
  let choices: [ChoiceModel]
}

This is a very simple data model, a question has a title and a list of choices, and each choice have a title and a valid flag to know the good answers.

So here our quiz template:

class Quiz {
  static var questions: [QuestionModel] = {
    // Question 1
    let c1 = ChoiceModel(title: "A powerful library", valid: true)
    let c2 = ChoiceModel(title: "A brush", valid: false)
    let c3 = ChoiceModel(title: "A swift implementation of ReactiveX", valid: true)
    let c4 = ChoiceModel(title: "The Observer pattern on steroids", valid: true)
    let q1 = QuestionModel(title: "What is RxSwift?", choices: [c1, c2, c3, c4])

    // Question 2
    let c5 = ChoiceModel(title: "Asynchronous events", valid: true)
    let c6 = ChoiceModel(title: "Email validation", valid: true)
    let c7 = ChoiceModel(title: "Networking", valid: true)
    let c8 = ChoiceModel(title: "Interactive UI", valid: true)
    let c9 = ChoiceModel(title: "And many more...", valid: true)
    let q2 = QuestionModel(title: "In which cases RxSwift is useful?", choices: [c5, c6, c7, c8, c9])

    return [q1, q2]
  }()
}

We define 2 questions with 3 and 5 choices each.

Creating the skeleton application

IB Screenshot

To continue you should copy the storyboard from the github project. As you can see above, we need a UINavigationBar for the title and the next button item, a UITableView for the choices (cells), and a submit button:

import UIKit

class ViewController: UIViewController {
  @IBOutlet weak var navigationBar: UINavigationBar!
  @IBOutlet weak var nextQuestionButton: UIBarButtonItem!
  @IBOutlet weak var choiceTableView: UITableView!
  @IBOutlet weak var submitButton: UIButton!
}

Now we are going to prepare the application for RxSwift. I suggest you to use cocoapods to integrate it in the project. So start by importing RxSwift and RxCocoa:

import RxCocoa
import RxSwift

The RxCocoa allows UIKit components to be reactive by providing useful pre-made methods. Now we are adding all the necessary variables we’ll need for the application:

let currentQuestionIndex                        = Variable(0)
let currentQuestion: Variable<QuestionModel?>   = Variable(nil)
let selectedIndexPaths: Variable<[NSIndexPath]> = Variable([])
let displayAnswers: Variable                    = Variable(false)
let disposeBag                                  = DisposeBag()

A variable represents a value that changes over time. It allows the observers to subscribe to the subject to receive value and all subsequent notifications. Here we have defined 4 variables:

  • currentQuestionIndex: to track the question index
  • currentQuestion: to track the question to display
  • selectedIndexPaths: to track the selected choices from the question choices list
  • displayAnswers: to know when display the answers

The last variable is a dispose bag. You always have to add subscriptions to a DisposeBag object in order for them to be safely disposed off when the subscription owner gets deallocated.

To finish the setup, the ViewController needs conforming to the UITableViewDelegate protocols and settings the choiceTableView as delegate:

class ViewController: UIViewController, UITableViewDelegate {
  override func viewDidLoad() {
    super.viewDidLoad()

    // Sets self as tableview delegate
    choiceTableView
      .rx_setDelegate(self)
      .addDisposableTo(disposeBag)
  }
}

We also need to prepare our cell to display choices:

import UIKit

final class ChoiceCell: UITableViewCell {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var checkboxImageView: UIImageView!
  @IBOutlet weak var checkmarkImageView: UIImageView!

  private let redColor   = UIColor(red: 231 / 255, green: 76 / 255, blue: 60 / 255, alpha: 1)
  private let greenColor = UIColor(red: 46 / 255, green: 204 / 255, blue: 113 / 255, alpha: 1)

  var choiceModel: ChoiceModel? {
    didSet {
      layoutCell()
    }
  }

  override var selected: Bool {
    didSet {
      checkmarkImageView.hidden = !selected

      layoutCell()
    }
  }

  var displayAnswers: Bool = false {
    didSet {
      layoutCell()
    }
  }

  private func layoutCell() {
    titleLabel.text = choiceModel?.title

    if let choice = choiceModel where displayAnswers {
      checkboxImageView.tintColor  = selected ? choice.valid ? greenColor : redColor : choice.valid ? greenColor : .blackColor()
      checkmarkImageView.tintColor = selected ? choice.valid ? greenColor : redColor : .blackColor()
    }
    else {
      checkboxImageView.tintColor  = .blackColor()
      checkmarkImageView.tintColor = .blackColor()
    }
  }
}

Now we are going to add the application behaviours.

Main part: Reactive behaviour

The first thing we are going to interact with is the currentQuestionIndex:

private func setupCurrentQuestionIndexObserver() {
  currentQuestionIndex
    .asObservable()
    .map { $0 % Quiz.questions.count }
    .subscribeNext { index -> Void in
      self.currentQuestion.value = Quiz.questions[index]
    }
    .addDisposableTo(disposeBag)
}

Here we are tracking the currentQuestionIndex value, then we map (transform) the value to be sure the index will be inside the question array, and we update the currentQuestion variable with the corresponding question. Notes that I explained before we need to dispose the subscription at the end.

Now let’s track the currentQuestion variable:

private func setupCurrentQuestionObserver() {
  currentQuestion
    .asObservable()
    .subscribeNext { question in
      self.navigationBar.topItem?.title = question?.title
    }
    .addDisposableTo(disposeBag)

  currentQuestion
    .asObservable()
    .filter { $0 != nil }
    .map { $0!.choices }
    .bindTo(choiceTableView.rx_itemsWithCellIdentifier("ChoiceCell", cellType: ChoiceCell.self)) { (row, element, cell) in
      cell.choiceModel = element
    }
    .addDisposableTo(disposeBag)
}

In a first place we are updating the title text each time the currentQuestion change. Then, if there is a question, we bind the question choices to the tableview. So each times the currentQuestion is updated the choiceTableView is reloaded to display the corresponding choice.

To update the current question we need to track the next button item events:

private func setupNextQuestionButtonObserver() {
  nextQuestionButton
    .rx_tap
    .subscribeNext {
      self.displayAnswers.value       = false
      self.currentQuestionIndex.value += 1
    }
    .addDisposableTo(disposeBag)
}

Here each time the button is tapped we ensure to hide the answers and we update the currentQuestionIndex.

We need to track the selected items to enable/disable the submit button:

private func setupChoiceTableViewObserver() {
  choiceTableView
    .rx_itemSelected
    .subscribeNext { indexPath in
      self.selectedIndexPaths.value.append(indexPath)
      self.choiceTableView.cellForRowAtIndexPath(indexPath)?.selected = true
    }
    .addDisposableTo(disposeBag)

  choiceTableView
    .rx_itemDeselected
    .subscribeNext { indexPath in
      self.selectedIndexPaths.value = self.selectedIndexPaths.value.filter { $0 != indexPath }
      self.choiceTableView.cellForRowAtIndexPath(indexPath)?.selected = false
    }
    .addDisposableTo(disposeBag)
}

Each time an item is selected (or deselected) we update the selectedIndexPaths and the cells.

We are enabling the submit button when at least one item is selected and answer is not displayed:

private func setupSubmitButtonObserver() {
  Observable
    .combineLatest(selectedIndexPaths.asObservable(), displayAnswers.asObservable()) { (s, d) in
      return s.count > 0 && !d
    }
    .bindTo(submitButton.rx_enabled)
    .addDisposableTo(disposeBag)

  submitButton
    .rx_tap
    .subscribeNext {
      self.displayAnswers.value = true
    }
    .addDisposableTo(disposeBag)
}

When the submit button is tapped we are displaying answers:

private func setupDisplayAnswersObserver() {
  displayAnswers
    .asObservable()
    .subscribeNext { displayAnswers in
      for cell in self.choiceTableView.visibleCells as! [ChoiceCell] {
        cell.displayAnswers = displayAnswers
      }
    }
    .addDisposableTo(disposeBag)
}

To finish we are implementing some UITableView delegate methods to improve the application behaviour:

func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
  return displayAnswers.value ? nil : indexPath
}

func tableView(tableView: UITableView, willDeselectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
  return displayAnswers.value ? nil : indexPath
}

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  cell.selectionStyle = .None
}

func tableView(tableView: UITableView, editingStyleForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCellEditingStyle {
  return .None
}

It allows the tableview to select cells when the answers are hidden, and conversely, it prevents to interact with when the answers are displayed.

Conclusion

If you are familiar with the UITableView and UICollectionView, it is worth to investigate the powerful the Rx world. With few line of codes you can create amazing behaviour without managing the state complexity of variable interactions.

Here you can find the example project.

Notes: you can find a french traduction of this tutorial on swift-tuto: Rendez vos UITableView plus réactive avec RxSwift.
1 Star2 Stars3 Stars4 Stars5 Stars (16 votes, average: 4.19 out of 5)

Loading...

4 comments

Cancel

Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  1. Ivan · January 16, 2016

    You put your models in your UITableViewCell?? That’s about where I stopped reading…

    • Yannick Loriot · January 16, 2016

      I know, this may be debatable. For me the view is the Storyboard while the UITableViewCell is more like a ViewModel or a Controller. Here the UITableViewCell is responsible of its own state. So it needs to have a link to its model and update its state when the model is updated.

  2. Darrarski · January 16, 2016

    You are capturing `self` strongly in each closure. I am not sure how RxSwift handles passed closures, but it smells like retain-cycle issue. Did you check for memory leaks?

  3. Pingback: Make UITableView more Reactive with RxSwift - Dariusz Rybicki