Using RxSwift to Populate Data on UITableView (iOS and Swift)
It's been a while since I posted. But better late than never. In today's blog post, I am going to write about how to use RxSwift to populate data in UITableView
on iOS.
RxSwift is a library for composing asynchronous and event-based code by using observable sequences and functional style operators, allowing for parameterized execution via schedulers. (from Kodeco.com)
Pre-requisites
Before we get started with RxSwift
, you need to add RxSwift
dependencies in your Xcode project via Cocoapods (Or any other suitable dependency management system on iOS). For my project specification, podfile
looks like this,
target 'TravelPlanner' do
use_frameworks!
pod 'RxSwift'
pod 'RxCocoa'
pod 'RxSwiftExt'
pod 'RxDataSources'
end
After you save this file, run pod install
from the command line and open <project_name>.xcworkspace
file.
Building Blocks
In order to build an app that can download and apply data to UITableView
through RxSwift
, we are going to start with basic building blocks which will form the foundation of the entire app we are building.
Building Views
The first thing we are going to do is build views. To keep it short and simple, I am not going to build everything from scratch, but we will make assumptions about the layout as we go along.
For our example, we are using a search bar and a table view. As the user is typing in the search bar, the app will input the entered text and send a request for locations matching with input text.
import UIKit
import RxSwift
import RxDataSources
final class SearchViewController: UIViewController {
let dataSource: RxTableViewSectionedReloadDataSource<SectionModel<String, Station>>
private lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
return view
}()
private lazy var searchBar: UISearchBar = {
UIBarButtonItem.appearance(whenContainedInInstancesOf:[UISearchBar.self]).tintColor = .white
let bar = UISearchBar()
bar.placeholder = "Waar wilt u naartoe? (minimaal 2 karakters)"
bar.searchTextField.backgroundColor = .white
bar.barTintColor = .black
return bar
}()
private lazy var searchResultsTableView: UITableView = {
let view = UITableView()
view.register(UITableViewCell.self, forCellReuseIdentifier: "Station")
return view
}()
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// An utility method to set up views
setupViews()
// An utility method to set up view constraints
setupConstraints()
}
//MARK: Private methods
private func setupViews() {
// Set up views
}
private func setupConstraints() {
// Set up constraints
}
}
After we run the app, this is how our UI will look like,
Building a Codable Station struct
As the downloaded location object represents the train station, we will create a new Codable
struct called Station
.
struct Station: Decodable {
let code: String
let fullName: String
}
Building a Network Service
Since we need to download data from the network, we will build a mock network service. This is just a dummy service for example, but you can replace this part with the actual service that is responsible for making a network call.
import Foundation
import RxSwift
import RxCocoa
struct StationsService {
private let urlSession: URLSession
private let decoder: JSONDecoder
init(urlSession: URLSession = .shared, decoder: JSONDecoder = JSONDecoder()) {
self.urlSession = urlSession
self.decoder = decoder
}
func searchStations<T: Decodable>(for type: T.Type, route: APIRoute) -> Observable<[T]> {
return urlSession.rx.data(request: ..<url_request>).map { data -> T in
do {
return try decoder.decode([T].self, from: data)
}
}
}
}
Building a View Model
In order to support downloading and displaying locations, we will use the view model and inject it into a view controller.
The view model is responsible for following tasks
- Intercepting input text and sending a network request to download the list of locations with the search query
- Converting downloaded location models into section view models and notifying the view to update the table view with these view models
The view model will have two extra structs inside it. One for input and another for output. Input will observe for any change in the search text and Output will publish any changes with station sections.
import Foundation
import RxSwift
import RxDataSources
final class SearchViewModel {
var input: Input?
var output: Output?
struct Input {
// Input search text
let searchText: AnyObserver<String?>
}
struct Output {
// List of station sections which are returned based on the search text entered by user
let stationSections: Observable<[SectionModel<String, Station>]>
}
private let stationsService: StationsService
init(stationsService: StationsService) {
let searchText = PublishSubject<String?>()
self.stationsService = stationsService
//We are delaying sending request for input by 0.5 seconds
let stations = searchText
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)
.flatMapLatest { searchText -> Observable<[Station]> in
guard let searchText, searchText.count > 2 else {
return .just([])
}
return stationsService.searchStations(for: [Station].self, route: .searchStations(query: searchText))
}.catchError { [weak self] error in
self?.errorMessageSubject.onNext(error.localizedDescription)
return .just([])
}
let stationSections = Observable.combineLatest(stations, searchText).map { [weak self] (elements, searchText) -> [SectionModel<String, Station>] in
guard let self else { return [] }
return [SectionModel(model: "Current Search", items: elements)]
}
input = Input(
searchText: searchText.asObserver()
)
output = Output(
stationSections: stationSections
)
}
}
As you can see, there is a lot going on in the view model. First, we initialize the view model with the network service object. Then, we have an observer on the input search text. To avoid making duplicate and repetitive requests, we have added a delay of 500ms every time the user types in the text.
As soon as the text is entered and captured, we make a call to the network service with the input text. Service responds with the list of Station
objects which is then mapped to a list of SectionModel
objects. Each section model represents a table view section and associated list of station locations.
Adding Bindings in View Controller
Now we are done with view model bindings. In the next part, we see how to perform RxSwift
bindings in the view controller.
View controller bindings are done for two reasons,
- To bind the input search text to
searchText
observer property on the view model's input struct - To bind the view model-provided station sections to the data source and attach it to the table view to populate the list data
Please note that we will also need a DisposeBag
object to store observable otherwise those are immediately released from memory and updates will be lost
Here's the partial source code for bindings,
.....
...
private let bag = DisposeBag()
..
....
init() {
....
..
self.dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Station>>(
configureCell: { (_, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Station")!
cell.textLabel?.text = element.name.fullName
return cell
},
titleForHeaderInSection: { dataSource, sectionIndex in
return dataSource[sectionIndex].model
}
)
}
private func setupRx() {
guard let input = viewModel.input, let output = viewModel.output else { return }
searchBar.rx
.value.orEmpty
.bind(to: input.searchText)
.disposed(by: bag)
output
.stationSections
.bind(to: searchResultsTableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
}
As you can see, with theRxSwift
, we saved on a lot of boilerplate code by eliminating routine table view datasource and delegate methods and replacing them with theRxTableViewSectionedReloadDataSource
object
Running the App and Verifying the Results
Now that all the building blocks are ready, let's run the app and see the result.
As you can see, when the app starts for the first time, no results are displayed. As they start typing in the search bar, the text is recognized with a 500ms delay, the network request is sent with the search keyword and search results are populated in the TableView
.
Conclusion
So that was all about how to use RxSwift to show data on UITableView
in iOS using Swift. Hope this tutorial was helpful to you. RxSwift is a reactive iOS framework that is responsible for implicitly performing many operations. We saw one such operation - Applying data to table view via data sources without using iOS's native data source and delegate conventions.
If you have any questions or concerns about this article, please feel free to reach out to me on LinkedIn.
Support and Feedback
If you like my blog content and wish to keep me going, please consider donating on Buy Me a Coffee or Patreon. Help, in any form or amount, is highly appreciated and it's a big motivation for me to keep writing more articles like this.
Consulting Services
I also provide a few consulting services on Topmate.io, and you can reach out to me there too. These services include,