How to integrate RxSwift in your MVVM architecture
It took me quite some time to get into Reactive Programming and its variant adapted for iOS development with RxSwift and RxCocoa. However, being fan of MVVM architecture and using an observer design pattern with it, it was natural for me to revisit my approach and use RxSwift instead. Thats what I’m going to cover in this post.
Prerequisite
If you didn’t get chance to look into Reactive Programming in Swift, there are a lot resources online you can look into. On the other side, if you are not familiar with MVVM architecture in Swift, I can only recommend you to start here.
I’m actually going to reuse the Currency Converter app previously used for the MVVM introduction and update the code and use RxSwift. That is the best way to get a vue of before / after and what we get from that migration.
First steps
To make it as easy as possible to migrate the code, I’m going to go step by step and update each files along the way, starting first with the protocol that defined my service.
// old code
protocol CurrencyServiceProtocol : class {
func fetchConverter(_ completion: @escaping ((Result<Converter, ErrorResult>) -> Void))
}
// new code
protocol CurrencyServiceObservable : class {
func fetchConverter() -> Observable<Converter>
}
First, I’ve defined a new service protocol returning an observable. Notice that I have changed the returned argument from Result
in completion handler to Converter
in my observable. The reason behind is RxSwift has its own way to handle error, therefore the Result
state in less necessary. Let’s leave it for now.
The same changes apply to the parser
// old code
switch T.parseObject(dictionary: dictionary) {
case .failure(let error):
completion(.failure(error))
break
case .success(let newModel):
completion(.success(newModel))
break
}
// new code
switch T.parseObject(dictionary: dictionary) {
case .failure(let error):
return Observable.error(error)
case .success(let newModel):
return Observable.just(newModel)
}
Instead of using completion block with a specific state result, we return an observable of the element: Observable.error
or Observable.just
defining our both states.
Finally we need to implement our observable protocol to our previous service. Here I’m using a mocked service instead of an API for testing purpose
extension FileDataService : CurrencyServiceObservable {
func fetchConverter() -> Observable<Converter> {
// giving a sample json file
guard let data = FileManager.readJson(forResource: "sample") else {
return Observable.error(ErrorResult.custom(string: "No file or data"))
}
return ParserHelper.parse(data: data)
}
}
Great! So far we converted our service to return an observable element with RxSwift, as well as our former ParserHelper
. Now let’s see what’s left for the View and ViewModel.
ViewModel and RxSwift
My ViewModel is the one that requires the most changes. It used to have a service, a datasource, and an error handler. However, we’ve seen how the error can be triggered in RxSwift, so we only need to catch it on the view side.
Same for the datasource, RxSwift includes wrappers around UITableView
and UICollectionView
that won’t require us to do much work on the ViewModel side, so I removed it as well.
However, if we remove the datasource, we still need something to keep a reference of CurrencyRate
. Since the ViewModel is by definition between View and Model, it’s best to leave it to him.
Here what’s left:
struct CurrencyViewModel {
weak var service: CurrencyServiceObservable?
// outputs
let rates : Observable<[CurrencyRate]>
init(service: CurrencyServiceObservable = FileDataService.shared) {
self.service = service
rates = service.fetchConverter()
.map({ $0.rates })
}
}
Notice that I’ve changed each protocols to use my new observable service. The ViewModel looks quite simple so far. Let’s see what’s left in the View.
View and RxSwift
First we’re going to bind our the data to our UITableView
. This was the work of our previous dataSource but since our View is quite simple, let’s keep the code simple as well.
private func bindViews() {
// bind data to tableview
self.viewModel.rates
.bind(to: self.tableView.rx.items(cellIdentifier: “CurrencyCell”, cellType: CurrencyCell.self)) { (row, currencyRate, cell) in
cell.currencyRate = currencyRate
}
.disposed(by: disposeBag)
}
But what if an error happen?
That’s where we can take advantage of RxSwift and catch the error in the same subscription
private func bindViews() {
// bind data to tableview
self.viewModel.rates
.subscribeOn(MainScheduler.instance)
.catchError { [weak self] error -> Observable<[CurrencyRate]> in
self?.showError(error as? ErrorResult)
return Observable.just([])
}
.bind(to: self.tableView.rx.items(cellIdentifier: "CurrencyCell", cellType: CurrencyCell.self)) { (row, currencyRate, cell) in
cell.currencyRate = currencyRate
}
.disposed(by: disposeBag)
}
In that example, any error returned would be caught and the view will show an error to the user. At the same time, it would return a new empty array to clean and reload the UITableView
.
Another quick note regarding this observable, notice we subscribe on MainScheduler
. This is to make sure each block will be executed on the main thread: we’ll display the error dialog properly.
We could have kept our dataSource and convert it as well to use Observable
to keep a separation of concern but I’m not sure it was worth it here. if you want to go further in that direction, I would suggest to look into RxDataSources that goes even further in DataSource manipulation.
We haven’t covered our code of unit tests and update previous one, that’s intentional, we’ll cover that in a separated post.
At the end, we’ve managed to update our MVVM architecture to integrate functional programming with RxSwift and take advantage of its observable pattern to simplify our code. This code is available on Github under rxswift
branch.
There are still many other ways to integrate RxSwift in your architecture, I have only covered a simple case. Regardless of the code approach and tool used, always keep in mind what you want to achieve, keep a clean code easy to test and maintain with a clear logic and following a separation of concern.
Thanks for reading!
Where to go from here