Using RxSwift to Populate Data on UITableView (iOS and Swift)

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

  1. Intercepting input text and sending a network request to download the list of locations with the search query
  2. 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,

  1. To bind the input search text to searchText observer property on the view model's input struct
  2. 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 the RxSwift, we saved on a lot of boilerplate code by eliminating routine table view datasource and delegate methods and replacing them with the RxTableViewSectionedReloadDataSource 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.

0:00
/

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,

  1. Let's Connect
  2. Resume Review
  3. 1:1 Mentorship
  4. Interview Preparation & Tips
  5. Conference Speaking
  6. Take-home Exercise Help (iOS)
  7. Career Guidance
  8. Mock Interview