Getting Started With RxSwift and RxCocoa [FREE]
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:
Tap on a chocolate row to add that product to your cart:
Tap on the cart in the upper right-hand corner. On the next page, you can checkout or reset the cart:
Choosing Checkout results in a credit card entry form:
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:
- viewWillAppear(_:) : Before showing the view controller.
- 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:
-
Grab the shopping cart’s
chocolates
variable as anObservable
. -
Call
subscribe(onNext:)
on thatObservable
to discover changes to theObservable
’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 yourObservable
. You’ll keep getting these notifications until you either unsubscribe or dispose of your subscription. What you get back from this method is anObserver
conforming toDisposable
. -
Add the
Observer
from the previous step to yourdisposeBag
. 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:
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:
Tap on some chocolates — the number of items in the cart now automatically updates!
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:
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:
-
Call
bind(to:)
to associate theeuropeanChocolates
observable with the code that executes each row in the table view. -
By calling
rx
, you access the RxCocoa extensions for the relevant class. In this case, it’s aUITableView
. -
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. - Pass in a block for each new item. Information about the row, the chocolate at that row and the cell will return.
-
Take the
Disposable
returned bybind(to:)
and add it to thedisposeBag
.
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!
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:
-
Call the table view’s reactive extension’s
modelSelected(_:)
, passing in theChocolate
model type to get the proper type of item back. This returns anObservable
. -
Taking that
Observable
, callsubscribe(onNext:)
, passing in a closure of what should be done any time a model is selected (i.e., a cell is tapped). -
Within the closure passed to
subscribe(onNext:)
, add the selected chocolate to the cart. - Also in the closure, deselect the tapped row.
-
subscribe(onNext:)
returns aDisposable
. Add it to thedisposeBag
.
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!
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:
-
Add an
Observer
to the value of aBehaviorRelay
. -
Subscribe to that
Observable
to reveal changes tocardType
. -
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:
-
Return the the contents of the text field as an Observable value.
text
is another RxCocoa extension, this time toUITextField
. -
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, useMainScheduler
. -
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 betrue
. -
Take the
Observable
value you’ve created and subscribe to it, updating the validity of the text field based on the incoming value. -
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:
Tap the Checkout button, which will take you to the credit card input screen:
Type 4 into the Card Number text field. You’ll see the card image instantly change to Visa:
Delete the 4, and the card image will change back to unknown. Type in 55 , and the image will change to MasterCard.
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:
Tap the button to see a summary of what you bought and how you paid for it, as well as a little Easter egg:
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.
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK