I wrote this article as I was (finally!) able to play with RxSwift. I’m glad I found the time to get to know the framework, as it is getting more and more popular and the best way to learn is to open Xcode, try it out and write an article about it.
I have to say RxSwift isn’t easy. Sure you can find tutorials online and they promise to teach you the basics in a few minutes, but you have to understand a few concepts in order to use it. This is not a framework you can import and use to perform a specific task, like a networking framework or a library to scan for barcodes or to manipulate an image. I think RxSwift makes sense if you want to structure your project in a certain way, I actually like it and would consider using it for future projects, but it may be overkill sometimes. As many smart people say, you should pick the right tool for the right job.
What is RxSwift?
Let me start sharing some link so you can find documentation about ReactiveX and its Swift implementation, called RxSwift.
Go to http://reactivex.io/ to read all about ReactiveX, available in multiple platforms and languages like Javascript, Swift, .NET, Java.
To find out more about RxSwift you can go to the github page of the project at this link https://github.com/ReactiveX/RxSwift
I think the easiest way to start grasping the concept behind ReactX is starting with the name, you write your code so you can react to changes asynchronously. The concept is not new, in Objective-C you could use Key Value Observing, in Swift you can use didSet on a variable to do something similar, but it isn’t easy to build a whole library on your own and if you use RxSwift chances are other developers will be able to jump into your code and make changes as they are familiar with it.
In this article we’ll use RxSwift and in particular RxCocoa, the framework we can use to have reactive extensions to Cocoa APIs.
I’m using the same example project on GitHub https://github.com/gualtierofrigerio/NetworkingExample I created to talk about Futures and Promises (check out my article at this link https://www.gfrigerio.com/callbacks-vs-promises/ ) in fact I’ll keep using the same code but I’ll show how we can implement a simple project with a table and a detail view controllers with and without RxSwift and RxCocoa while using Promises to make the network calls.
If you want to try the project remember to run pod install to get RxSwift, as I din’t add the Pods directory on GitHub.
The sample project
I won’t go into details about the project as you can refer to the article linked above. Let’s say we have a set of data, in this case users, and we want to display them in a table view, then show their details in a separate view. I haven’t used Storyboard for the example as I build the views twice, the first time in the “classic” way and then using RxCocoa, so in the sources you’ll find some boilerplate to create stack views and labels, boring stuff I’m not going to talk about in detail.
Let’s start by building a table view as you’d normally do, the code can be found at this link https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/UsersTableViewController.swift
This class is an extension of UITableViewController so we have the usual datasource and delegate functions like numberOfSections, numberOfRowsInSection and cellForRowAt you’re familiar with. We display a list of users and we have a UISearchController so we can quickly find a user matching a query.
I used two variables, one for the users you can set via setUsers, and filteredUsers containing a subset of users matching the current query.
func updateSearchResults(for searchController: UISearchController) {
if let searchText = searchController.searchBar.text {
applyFilter(searchText)
}
else {
applyFilter("")
}
}
private func applyFilter(_ filter:String) {
if filter.count > 0 {
filteredUsers = users.filter({
return $0.username.lowercased().contains(filter.lowercased())
})
}
else {
filteredUsers = users
}
tableView.reloadData()
}
The ViewController conforms to UISearchResultsUpdating protocol, so I implemented the updateSearchResults to be notified every time the text changes in the UISearchController. I then call the applyFilter function, update the filteredUsers array and call reloadData on the tableView, so the aforementioned data source functions like numberOfRowsInSection and cellForRowAt will get called to refresh the UITableView.
There’s nothing wrong with this implementation, it is quite common to deal with Table Views this way, but I needed to implement a delegate method to be notified about an event, in this case the user writing text, and then perform the action of filtering data and forcing the table view to update itself. How do I show the detail view controller? I need to be the UITableView delegate and implement didSelectRowAt, then I can get the user looking at the indexPath and create the new view controller
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let user = filteredUsers[indexPath.row]
showUser(user)
}
private func showUser(_ user:User) {
let userVC = UserViewController()
userVC.setUser(user)
self.navigationController?.pushViewController(userVC, animated: true)
}
Again, nothing wrong with it.
Let’s take a look at the detail view, implemented in this file https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/UserViewController.swift
As I mentioned before, much of the code is just setting up the UI so I’m not going into detail. Just notice I configure the view in configureView and I use two instance variables to keep track of the UILabels I’m going to interact with (username and mail address), so when the user variable is set I can set these two fields by accessing the variables. It’s ok, but I’ll show a better way to do the same thing with RxCocoa.
Using RxSwift and RxCocoa
It is now time to look at the Rx implementation of the same views. The implementation is different, but they provide the same functionalities and look exactly the same. Actually even classes and files have a very similar name, with an Rx suffix.
Let’s start with the table view implementation you can find here https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/UsersTableViewControllerRx.swift
The code is more compact, and the class is not conforming to UITableViewDataSource and UITableViewDelegate, so someone else will take care of it for us, and guess what, it is RxCocoa.
Before we get to that I need to introduce 3 variables: filteredUsers, filterText and disposeBag.
We want to use RxSwift and this is about observing value changes, so we need our variables to be wrapped around an Rx class. I’m using BehaviorRelay as Variable is deprecated. I guess Variable was an easiest name to deal with, and I know many tutorials you find online still have sample code using Variable. Maybe RxSwift will have a typealias to keep the Variable name around, but to be on the safe side I used BehaviourRelay.
private var users = [User]()
private let filteredUsers:BehaviorRelay<[User]> = BehaviorRelay(value: [])
private let filterText:BehaviorRelay<String> = BehaviorRelay<String>(value:"")
func setUsers(_ users:[User]) {
self.users.accept(users)
filterText.accept("")
}
private func initialSetup() {
filterText.subscribe(onNext: { [weak self] text in
self?.filterUsers(withString: text)
...
}
@discardableResult private func filterUsers(withString filter:String) -> [User] {
var filteredUsers = users
if filter.count > 0 {
filteredUsers = users.filter({
return $0.username.lowercased().contains(filter.lowercased())
})
}
self.filteredUsers.accept(filteredUsers)
return filteredUsers
}
If you’re not familiar with generics (The <[User]> I put in the type) I wrote about them in the article linked at the beginning.
As you can see we don’t access the BehaviourRelay variable directly, but we can read its value and when we need to change it we call accept and pass the new value.
Why do we need BehaviourRelay? Because we can observe its value changes and react to them. We could write code to observe users, and bind its value to the TableView so every time new users are added we can see them.
And what is DisposeBag? We need it for memory management, after we observe the value of a variable with Rx we add the observer to the DisposeBag, so when the object is deallocated the observer is unsubscribed to the object it was observing and ARC can deallocate the object and avoid memory leaks.
The idea here is to have the original users as a standard Array, just like I did in the previous implementation of the table, then have the filtered users and the text used to filter them as observable variables, so we can apply the filter as soon as the string changes, and after applying the filter we can refresh the table view. Let’s see how we can do it by configuring the UITableView
private func configureTableView() {
view.addSubview(tableView)
tableView.frame = view.frame
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
filteredUsers.asObservable()
.bind(to: tableView.rx
.items(cellIdentifier: cellIdentifier,
cellType: UITableViewCell.self)) {
row, user, cell in
self.configureCell(cell, withUser: user)
}
.disposed(by:disposeBag)
...
}
As I said we want to observe a value and react to its changes, and that’s what asObservable is about. We do have an Observable<Element> and we can bind it to our UITableView, since we imported RxCocoa our UIKit elements like UITableView have a variable rx we can access.
We don’t implement the UITableView data source, but RxCocoa gives us this items function we can access after binding a variable to the UITableView, we can configure a cellIdentifier and a class for the cell, I’m using a standard UITableViewCell in this example. We then have a closure with the row, the user connected to the row and the cell so we can configure the cell. Feels like magic? We provided an array of User and items calls the closure for each element of the array with the cell it will be bound to.
At the end we call disposed(by:) so we can add this observer to the dispose bag and be ok with memory management. Looks cool, maybe is not very easy and it takes time to get used to it, but I like the way I can bind an array to a table without implementing the data source methods I’m so used to, as they’re most boring (return the array length, check the index path row to get the right element…).
Let’s see another example of reactive programming. We do have a feature in our app, there is a UISearchController and we want to update the UITableView every time the text changes. I think this is a great way to use RxSwift as the text values changes multiple times, and we don’t have to implement a delegate to be notified about that.
private func configureSearchViewController() {
searchController.hidesNavigationBarDuringPresentation = false
searchController.dimsBackgroundDuringPresentation = false
navigationItem.searchController = searchController
searchController.searchBar.rx.text
.subscribe(onNext: { [weak self] text in
self?.filterText.accept(text?.lowercased() ?? "")
})
.disposed(by: disposeBag)
}
The configuration of the UISearchController is similar to the one without Rx, but I didn’t set the delegate this time.
I don’t need to, as I can observe the text value just like I observed the array of users. The bind(onNext:) function is called every time new text is typed, we can set the value of filterText and that will trigger the filtering and eventually refresh the table. I like this approach, instead of having callbacks or delegate methods to listen to even I can observe changes and write code to deal with them on the spot.
There is one thing left, we need to open the detail view every time a cell is selected and we’re not the delegate of UITableView, so we’ll use RxCocoa again.
private func configureTableView() {
...
tableView.rx
.modelSelected(User.self)
.subscribe(onNext: { [weak self] user in
self?.showUser(user)
})
.disposed(by: disposeBag)
}
This is the final part of function where we setup the binding, where we have to react to the user tapping on a cell. The function modelSelected takes care of that, and returns an Observable sequence. We can then subscribe to that sequence change to get the selected value, I only provided the onNext parameter but you can also specify onError if you have to deal with something that can fail, it is not our case as we can assume the user exists if a cell is bound to it, but you may find examples with onError and onComplete. At the end always remember to call disposed, so you’re ok with memory management. Time to see how the detail view controller can be implemented with RxSwift. Take a look at the implementation https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/UserViewControllerRx.swift
Much of the implementation is dealing with boring UI stuff, so let’s see only the things related to Rx.
struct UserViewModel {
let username = BehaviorRelay<String>(value:"")
let email = BehaviorRelay<String>(value:"")
func update(withUser user:User) {
self.username.accept(user.username)
self.email.accept(user.email)
}
}
func setUser(_ user:User) {
viewModel.update(withUser: user)
}
I’m using the UserViewModel struct so I can have some rx variable there an I can update it with a user, more neat than having all the variables in the view controller. Every time a new user is set I want the labels to be updates, and again RxCocoa will help me.
let mailValue = UILabel()
let usernameValue = UILabel()
// configure the labels...
viewModel.username
.asObservable()
.map{$0.uppercased()}
.bind(to:usernameValue.rx.text)
.disposed(by:disposeBag)
viewModel.email
.asObservable()
.bind(to:mailValue.rx.text)
.disposed(by: disposeBag)
And that’s it, we can observe the changes and bind them to the UIKit element, in this case the label. Just to demonstrate something new I decided to print the username upper cased, so you can see I have .map{$0.uppercased()} after .asObservable(), that allows me to perform some kind of transformation on the data before binding it to the UI component. It is not mandatory, as you can see you can bind the Observable value directly as I did for the email, but I wanted to show you it is possible to perform other operations on the Observable.
I hope you enjoyed the article and it helped you grasping some of the concepts behind RxSwift. It isn’t easy, playing with it and writing this article was worth the time spent.
What’s next? I plan to update my GitHub project to use RxSwift even for the network layer, while now I’m using my implementation of a Promise, then I’ll update the article so you’ll have a more exhaustive example of an app written with RxSwift.