46

Getting Started With RxSwift and RxCocoa [FREE]

 5 years ago
source link: https://www.tuicool.com/articles/hit/yMvQZ3Y
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Update note : Ron Kliffer updated this tutorial for Xcode 10, Swift 4.2, iOS 12 and RxSwift 4.4.0. Ellen Shapiro wrote the original.

It’s great when code does exactly what you want (unlike my cat). Change the program, tell the code to update, and it does. Good code!

Most programming in the object-oriented era has been imperative . Code tells your program what to do and has many ways to listen to changes. However, you must tell the system when something changes.

Wouldn’t it be better if you could set things up so the code updates reflect changes automatically? That’s the idea of reactive programming: Your application can react to changes in the underlying data without you telling it to do so. This makes it easier to focus on the logic at hand rather than maintaining a particular state.

You can achieve this in Swift through Key-Value Observation and using didSet , but it can be cumbersome to set up. Alternatively, there are several frameworks in Swift that facilitate reactive programming.

Note : For more background, see Colin Eberhardt’sgreat article outlining the differences between major frameworks. Check out the comments section for further discussion.

In this tutorial, you’ll use the RxSwift framework and its companion RxCocoa to take a chocolate-buying app from imperative to reactive.

What are RxSwift and RxCocoa?

RxSwift and RxCocoa are part of the suite of ReactiveX (Rx) language tools that span multiple programming languages and platforms.

While ReactiveX started as part of the .NET/C# ecosystem, it’s grown extremely popular with Rubyists, JavaScripters and, particularly, Java and Android developers.

RxSwift is a framework for interacting with the Swift programming language, while RxCocoa is a framework that makes Cocoa APIs used in iOS and OS X easier to use with reactive techniques.

ReactiveX frameworks provide a common vocabulary for tasks used repeatedly across different programming languages. This makes it easy to focus on the syntax of the language itself rather than figuring out how to map a common task to each new language.

Observables and Observers

Two key concepts are the Observable and the Observer .

  • An Observable emits notifications of change.
  • An Observer subscribes to an Observable and gets notified when that Observable has changed.

You can have multiple Observers listening to an Observable. When the Observable changes, it will notify all its Observers.

The DisposeBag

The DisposeBag is an additional tool RxSwift provides to help deal with ARC and memory management. Deallocating a parent object results in the disposal of Observer objects in the DisposeBag.

When deinit() is called on the object that holds the DisposeBag, each disposable Observer is automatically unsubscribed from what it was observing. This allows ARC to take back memory as it normally would.

Without a DisposeBag, you’d get one of two results. Either the Observer would create a retain cycle, hanging on to what it’s observing indefinitely, or it could be deallocated, causing a crash.

To be a good ARC citizen, remember to add any Observable objects to the DisposeBag when you set them up. The DisposeBag will clean up nicely for you.

Getting Started

It’s time for you to get to the chocolate! Use the Download Materials button at the top or bottom of this tutorial to download the starter project. After you’ve done so, open Chocotastic.xcworkspace .

Build and run the application. You’ll see the following screen, listing several kinds of chocolate you can buy from Europe, along with their respective prices:

initial_landing-282x500.png

Tap on a chocolate row to add that product to your cart:

added_chocolate-282x500.png

Tap on the cart in the upper right-hand corner. On the next page, you can checkout or reset the cart:

checkout-282x500.png

Choosing Checkout results in a credit card entry form:

cc_form-282x500.png

Later in the tutorial, you’ll come back to set this up using purely reactive programming. For now, tap the Cart button to return to the cart summary. Then, tap the Reset button to return to the main screen with an empty cart.

The Starting Point: Nonreactivity

Now that you’ve seen what the application does, it’s time to examine how it works. Open ChocolatesOfTheWorldViewController.swift . You’ll see some standard UITableViewDelegate and UITableViewDataSource extensions.

Look at updateCartButton() . This method updates the cart button with the current number of chocolates in the cart. The method updates the cart in two instances:

  1. viewWillAppear(_:) : Before showing the view controller.
  2. tableView(_:didSelectRowAt:) : After a user adds a new chocolate to the cart.

These are both imperative ways of changing the count. You must explicitly call the method to update the count.

You’re going to rewrite the code to use a reactive technique. That way, the button will update on its own.

RxSwift: Making the Cart Count Reactive

The methods referring to items in the cart use a ShoppingCart.sharedCart singleton. Open ShoppingCart.swift and you’ll see a standard setup of a variable on the singleton instance:

var chocolates: [Chocolate] = []

Right now, you can’t observe changes to the contents of chocolates . You could add a didSet closure to its definition, but that would only get called when the entire array updates, rather than any of its elements.

Fortunately, RxSwift has a solution. Replace the line creating the chocolates variable with this:

let chocolates: BehaviorRelay<[Chocolate]> = BehaviorRelay(value: [])

Note : This change will cause errors in the sidebar. You’ll fix those in a moment.

This syntax can be a little hard to wrap your head around. It helps to understand what’s going on. Essentially, rather than setting chocolates to a Swift array of Chocolate objects, you’ve now defined it as a RxSwift BehaviorRelay that has a type of a Swift array of Chocolate objects.

BehaviorRelay is a class, so it uses reference semantics. This means that chocolates refers to an instance of BehaviorRelay .

BehaviorRelay has a property called value . This stores your array of Chocolate objects.

The magic of BehaviorRelay comes from a method called asObservable() . Instead of manually checking value every time, you can add an Observer to keep an eye on the value for you. When the value changes, the Observer lets you know so you can react to any updates.

The downside is that if you need to access or change something in that array of chocolates, you must do it via accept(_:) . This method on BehaviorRelay updates its value property. That’s why the compiler is throwing a tantrum and presenting a fistful of errors. Time to fix them!

In ShoppingCart.swift , find the method totalCost() and change this line:

return chocolates.reduce(0) {

To:

return chocolates.value.reduce(0) {

In itemCountString() , change:

guard chocolates.count > 0 else {

To:

guard chocolates.value.count > 0 else {

And change:

let setOfChocolates = Set<Chocolate>(chocolates)

To:

let setOfChocolates = Set<Chocolate>(chocolates.value)

Finally, change:

let count: Int = chocolates.reduce(0) {

To:

let count: Int = chocolates.value.reduce(0) {

In CartViewController.swift , find reset() and change:

ShoppingCart.sharedCart.chocolates = []

To:

ShoppingCart.sharedCart.chocolates.accept([])

Back in ChocolatesOfTheWorldViewController.swift , change the implementation of updateCartButton() to this:

cartButton.title = "\(ShoppingCart.sharedCart.chocolates.value.count) \u{1f36b}"

And in tableView(_:didSelectRowAt:) , change this line:

ShoppingCart.sharedCart.chocolates.append(chocolate)

To the following:

let newValue =  ShoppingCart.sharedCart.chocolates.value + [chocolate]
ShoppingCart.sharedCart.chocolates.accept(newValue)

Whew! That should make Xcode happy and take care of the errors. Now you can take advantage of reactive programming and observe chocolates !

Go to ChocolatesOfTheWorldViewController.swift and add the following to the list of properties:

private let disposeBag = DisposeBag()

This creates the DisposeBag you’ll use to clean up any Observers you set up.

Add the following in the extension under the //MARK: Rx Setup comment:

func setupCartObserver() {
  //1
  ShoppingCart.sharedCart.chocolates.asObservable()
    .subscribe(onNext: { //2
      [unowned self] chocolates in
      self.cartButton.title = "\(chocolates.count) \u{1f36b}"
    })
    .disposed(by: disposeBag) //3
}

This sets up a reactive Observer to update the cart automatically. As you can see, RxSwift makes heavy use of chained functions, meaning that each function takes the result of the previous function.

How that’s happening in this case:

  1. Grab the shopping cart’s chocolates variable as an Observable .
  2. Call subscribe(onNext:) on that Observable to discover changes to the Observable ’s value. subscribe(onNext:) accepts a closure that executes every time the value changes. The incoming parameter to the closure is the new value of your Observable . You’ll keep getting these notifications until you either unsubscribe or dispose of your subscription. What you get back from this method is an Observer conforming to Disposable .
  3. Add the Observer from the previous step to your disposeBag . This disposes of your subscription upon deallocating the subscribing object.

To finish, delete the imperative updateCartButton() . This will cause errors to appear where it was being called in viewWillAppear(_:) and tableView(_:didSelectRowAt:) .

To fix them, delete the entire viewWillAppear(_:) since calling updateCartButton() is the only thing it’s doing beyond calling super . Then, delete the call to updateCartButton() in tableView(_:didSelectRowAt:) .

Build and run. You’ll see the list of chocolates:

Simulator-Screen-Shot-Jul-4-2016-9.26.45-PM-282x500.png

Uh-oh. The cart button only says Item, and when you start tapping on the list of chocolates, nothing happens. What went wrong?

You created a function to set up your Rx Observers, but there’s nothing calling that function, so the Observers aren’t responding. To fix this, open ChocolatesOfTheWorldViewController.swift and add the following at the end of viewDidLoad() :

setupCartObserver()

Build and run the application to see the list of chocolates again:

initial_landing-282x500.png

Tap on some chocolates — the number of items in the cart now automatically updates!

added_chocolate-282x500.png

Success! You can now add all the chocolates to the cart.

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-282x500.png

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_landing-282x500.png

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-282x500.png

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 the disposeBag .

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 throttleInterval = 0.1

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

func setupTextChangeHandling() {
  let creditCardValid = creditCardNumberTextField
    .rx
    .text //1
    .distinctUntilChanged()
    .throttle(throttleInterval, 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
  .distinctUntilChanged()
  .throttle(throttleInterval, 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
  .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-282x500.png

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

cc_form-282x500.png

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

Simulator-Screen-Shot-Jul-4-2016-7.47.40-PM-282x500.png

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-282x500.png

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-282x500.png

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-282x500.png

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

Where to Go From Here?

Download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

If you want a challenge, try adding a couple things to make this application even more reactive:

CartViewController

Now that you’ve gotten a taste of Rx programming, here are a few more resources to help you continue learning:

Finally, our ownMarin Todorov has a great blog about his adventures in Reactive programming called rx_marin . Check it out!

If you enjoyed this tutorial, check outour RxSwift book, available in our store.

rx_book_cover-248x320.png

Here’s a taste of what’s in the book:

  • Getting Started : Get an introduction to the reactive programming paradigm, learn the terminology involved and see how to begin using RxSwift in your projects.
  • Event Management : Learn how to handle asynchronous event sequences via two key concepts in Rx — Observables and Observers.
  • Being Selective : See how to work with various events using concepts such as filtering, transforming, combining, and time operators.
  • UI Development : RxSwift makes it easy to work with the UI of your apps using RxCocoa, which provides an integration of both UIKit and Cocoa.
  • Intermediate Topics : Level up your RxSwift knowledge with chapters on reactive networking, multi-threading, and error handling.
  • Advanced Topics : Round out your RxSwift education by learning about MVVM app architecture, scene-based navigation, and exposing data via services.
  • Much, much more!

By the end of this book, you’ll have hands-on experience solving common issues in a reactive paradigm and be well on your way to coming up with your own Rx patterns and solutions!

Have questions or other Rx resources to suggest? Sound off below in the comments, or in the forums.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK