Getting Started With RxSwift and RxCocoa

Use the RxSwift framework and its companion RxCocoa to take a chocolate-buying app from annoyingly imperative to awesomely reactive. By Ron Kliffer.

4.6 (87) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

RxCocoa: Making the TableView Reactive

Now that you’ve made the cart reactive using RxSwift, you’ll use RxCocoa to make your UITableView reactive, too.

RxCocoa has reactive APIs for several different types of UI elements. These allow you to create table views without overriding delegate or data source methods.

To demonstrate this, delete the entire UITableViewDataSource and UITableViewDelegate extensions from ChocolatesOfTheWorldViewController.swift . Next, delete the assignments to tableView.dataSource and tableView.delegate from viewDidLoad().

Build and run the application, and you’ll see that your happy little table view full of chocolates has become sad and empty:

no chocolate for you

That’s no fun. Time to restore the chocolates!

A reactive table view needs something for the table view to react to. In ChocolatesOfTheWorldViewController.swift, update the europeanChocolates property to be an Observable:

let europeanChocolates = Observable.just(Chocolate.ofEurope)

just(_:) indicates that there won’t be any changes to the underlying value of the Observable, but that you still want to access it as an Observable value.

Note: Sometimes, calling just(_:) is an indication that using reactive programming might be overkill. After all, if a value never changes, why use a programming technique designed to react to changes? In this example, you’re using it to set up reactions of table view cells that will change. However, it’s a good idea to look carefully at how you’re using Rx. Just because you have a hammer doesn’t mean every problem is a nail. :]

Now, add the following function to the extension marked as //MARK: - Rx Setup:

func setupCellConfiguration() {
  //1
  europeanChocolates
    .bind(to: tableView
      .rx //2
      .items(cellIdentifier: ChocolateCell.Identifier,
             cellType: ChocolateCell.self)) { //3
              row, chocolate, cell in
              cell.configureWithChocolate(chocolate: chocolate) //4
    }
    .disposed(by: disposeBag) //5
}

What’s going on here:

  1. Call bind(to:) to associate the europeanChocolates observable with the code that executes each row in the table view.
  2. By calling rx, you access the RxCocoa extensions for the relevant class. In this case, it’s a UITableView.
  3. Call the Rx method items(cellIdentifier:cellType:), passing in the cell identifier and the class of the cell type you want to use. The Rx framework calls the dequeuing methods as though your table view had its original data source.
  4. Pass in a block for each new item. Information about the row, the chocolate at that row and the cell will return.
  5. Take the Disposable returned by bind(to:) and add it to the disposeBag.

The values normally generated by tableView(_:numberOfRowsInSection:) and numberOfSections(in:) are now automatically calculated based on the observed data. The closure effectively replaces tableView(_:cellForRowAt:).

Go to viewDidLoad() and add a line calling your new setup method:

setupCellConfiguration()

Build and run the application, and voilà! Your chocolates are back!

initial state

When you try to tap on each chocolate, however, they don’t appear in the cart. Did you break something with your earlier Rx method?

Nope! Removing tableView(_:didSelectRowAt:) took away anything which would recognize cell taps or know how to handle them.

To remedy this, there’s another extension method RxCocoa adds to UITableView called modelSelected(_:). This returns an Observable you can use to watch information about selected model objects.

In ChocolatesOfTheWorldViewController.swift, add the following method to the //MARK: - Rx Setup extension:

func setupCellTapHandling() {
  tableView
    .rx
    .modelSelected(Chocolate.self) //1
    .subscribe(onNext: { [unowned self] chocolate in // 2
      let newValue =  ShoppingCart.sharedCart.chocolates.value + [chocolate]
      ShoppingCart.sharedCart.chocolates.accept(newValue) //3
        
      if let selectedRowIndexPath = self.tableView.indexPathForSelectedRow {
        self.tableView.deselectRow(at: selectedRowIndexPath, animated: true)
      } //4
    })
    .disposed(by: disposeBag) //5
}

Here’s what that does, step by step:

  1. Call the table view’s reactive extension’s modelSelected(_:), passing in the Chocolate model type to get the proper type of item back. This returns an Observable.
  2. Taking that Observable, call subscribe(onNext:), passing in a closure of what should be done any time a model is selected (i.e., a cell is tapped).
  3. Within the closure passed to subscribe(onNext:), add the selected chocolate to the cart.
  4. Also in the closure, deselect the tapped row.
  5. subscribe(onNext:) returns a Disposable. Add it to the disposeBag.

Finally, go to viewDidLoad() and add a line calling your new setup method:

setupCellTapHandling()

Build and run. You’ll see your familiar list of chocolates, but now you can add them to your heart’s content!

added chocolate

RxSwift and Direct Text Input

RxSwift can both take and react to direct text input by the user.

To get a taste of handling text input reactively, try adding validation and card type detection to the credit card entry form.

A tangle of UITextFieldDelegate methods handle credit card entry in nonreactive programs. Often, each contains a mess of if/else statements indicating the actions and logic to apply based on the text field you’re editing.

Reactive programming ties the handling more directly to each input field and clarifies what logic applies to which text field.

Go to BillingInfoViewController.swift. Add the following after the other declared properties:

private let disposeBag = DisposeBag()

As before, this defines a DisposeBag to ensure disposal of all your Observables.

It’s nice for users to see what type of credit card they’re inputting based on known card types.

To do this, add the following to the extension below the //MARK: - Rx Setup comment:

func setupCardImageDisplay() {
  cardType
    .asObservable()
    .subscribe(onNext: { [unowned self] cardType in
      self.creditCardImageView.image = cardType.image
    })
    .disposed(by: disposeBag)
}

What’s going on here:

  1. Add an Observer to the value of a BehaviorRelay.
  2. Subscribe to that Observable to reveal changes to cardType.
  3. Ensure the observer’s disposal in thedisposeBag.

You’ll use this to update the card image based on changes to the card type. Now for the fun part: Text change handling.

Since a user might type quickly, running validation for every key press could be computationally expensive and lead to a lagging UI. Instead, debounce or throttle how quickly the user’s input moves through a validation process. This means you’ll only validate the input at the throttle interval rather than every time it changes. This way, fast typing won’t grind your whole app to a halt.

Throttling is a specialty of RxSwift since there’s often a fair amount of logic to be run when something changes. In this case, a small throttle is worthwhile.

First, add the following just below the other property declarations in BillingInfoViewController:

private let throttleIntervalInMilliseconds = 100

This defines a constant for the throttle length in milliseconds. Now add the following to the RX Setup extension:

func setupTextChangeHandling() {
  let creditCardValid = creditCardNumberTextField
    .rx
    .text //1
    .observeOn(MainScheduler.asyncInstance)
    .distinctUntilChanged()
    .throttle(.milliseconds(throttleIntervalInMilliseconds), scheduler: MainScheduler.instance) //2
    .map { [unowned self] in
      self.validate(cardText: $0) //3
  }
    
  creditCardValid
    .subscribe(onNext: { [unowned self] in
      self.creditCardNumberTextField.valid = $0 //4
    })
    .disposed(by: disposeBag) //5
}

What this code does:

  1. Return the the contents of the text field as an Observable value. text is another RxCocoa extension, this time to UITextField.
  2. Throttle the input to set up the validation to run based on the interval defined above. The scheduler parameter is a more advanced concept, but the short version is that it’s tied to a thread. To keep everything on the main thread, use MainScheduler.
  3. Transform the throttled input by applying it to validate(cardText:) provided by the class. If the card input is valid, the ultimate value of the observed boolean will be true.
  4. Take the Observable value you’ve created and subscribe to it, updating the validity of the text field based on the incoming value.
  5. Add the resulting Disposable to the disposeBag.

The code for the expiration date and CVV validation follows the sample pattern.
Add the following code to the bottom of setupTextChangeHandling():

let expirationValid = expirationDateTextField
  .rx
  .text
  .observeOn(MainScheduler.asyncInstance)
  .distinctUntilChanged()
  .throttle(.milliseconds(throttleIntervalInMilliseconds), scheduler: MainScheduler.instance)
  .map { [unowned self] in
    self.validate(expirationDateText: $0)
}
    
expirationValid
  .subscribe(onNext: { [unowned self] in
    self.expirationDateTextField.valid = $0
  })
  .disposed(by: disposeBag)
    
let cvvValid = cvvTextField
  .rx
  .text
  .observeOn(MainScheduler.asyncInstance)
  .distinctUntilChanged()
  .map { [unowned self] in
    self.validate(cvvText: $0)
}
    
cvvValid
  .subscribe(onNext: { [unowned self] in
    self.cvvTextField.valid = $0
  })
  .disposed(by: disposeBag)

Now that you’ve got Observable values set up for the validity of the three text fields, add the following at the bottom of setupTextChangeHandling():

let everythingValid = Observable
  .combineLatest(creditCardValid, expirationValid, cvvValid) {
    $0 && $1 && $2 //All must be true
}
    
everythingValid
  .bind(to: purchaseButton.rx.isEnabled)
  .disposed(by: disposeBag)

This uses Observable’s combineLatest(_:) to take the three observables you’ve already made and generate a fourth. The generated Observable, called everythingValid, is either true or false, depending on whether all three inputs are valid.

everythingValid reflects the isEnabled property on UIButton‘s reactive extension. everythingValid’s value controls the state of the purchase button.

If all three fields are valid, the underlying value of everythingValid will be true. If not, the underlying value will be false. In either case, rx.isEnabled will apply the value to the purchase button, which is only enabled when all the the credit card details are valid.

Now that you’ve created your setup methods, add the code to call them to viewDidLoad():

setupCardImageDisplay()
setupTextChangeHandling()

Build and run the application. To get to the credit card input, tap a chocolate to add it to the cart and then tap the cart button to go to the cart. As long as you have at least one chocolate in your cart, the checkout button should work:

checkout

Tap the Checkout button, which will take you to the credit card input screen:

Credit Card Form

Type 4 into the Card Number text field. You’ll see the card image instantly change to Visa:

Visa

Delete the 4, and the card image will change back to unknown. Type in 55, and the image will change to MasterCard.

Simulator Screen Shot Jul 4, 2016, 7.47.36 PM

Neat! This app covers the four major credit card types in the United States: Visa, MasterCard, American Express and Discover. If you have one of those types of credit cards, you can input the number to see the correct image pop up and check to see if the number is valid.

Note: If you don’t have one of those credit cards, use one of the test card numbers that PayPal uses to test their card sandbox. These should pass all local validation in the application, even though the numbers themselves are not usable.

Once a valid credit card number is input with an expiration date and CVV, the Buy Chocolate! button will enable:

enabled_checkout

Tap the button to see a summary of what you bought and how you paid for it, as well as a little Easter egg:

success

Congratulations! Thanks to RxSwift and RxCocoa, you can buy as much chocolate as your dentist will let you get away with. :]