RxSwift & MVVM - An alternative structure for your ViewModel
For last couple weeks, I’ve worked a lot about how to integrate RxSwift into an iOS project but I wasn’t fully satisfied with the view model. After reading many documentation and trying on my side, I’ve finally found a structure I’m happy with.
Before going further, if you didn’t get chance to try RxSwift with an MVVM architecture, I would recommend to start there.
When I started my research how to couple RxSwift and a ViewModel, a lot of solutions referred back to Kickstarter and their functional approach using Input and Output. The ViewModel would get Input from the View component and compute it into Output.
Input represents any event or interaction from the View. It can be a button pressed or a cell selected. Output would be any changes from the ViewModel that the View has to display.
I’ll share first a solution using protocols only to keep abstract the logic, then see what we can improve.
Protocols
I took an example of currency converter. In the Input, the user can select a currency or reload from a pull to refresh. In the Output, it can return a list of currencies and trigger an observable to display detail of currency.
protocol CurrencyViewModelInput {
var reload: AnyObserver<Void> { get set }
var selectCurrency: AnyObserver<Currency> { get set }
}
protocol CurrencyViewModelOutput {
var currencies : Driver<[Currency]> { get }
var showCurrencyDetail : Driver<Currency> { get }
}
protocol CurrencyViewModelType {
var input : CurrencyViewModelInput { get }
var output : CurrencyViewModelOutput { get }
}
Note: I use Driver here to make sure it runs on main instance since it aims to be displayed in the UI.
So far, it looks great, the idea is to keep the code self explained and easy to test. Those components are easily accessible from the ViewModel protocol.
viewModel.output.currencies.register(...)
viewModel.input.reload.onNext(...)
Let’s see how the ViewModel looks like.
struct CurrencyViewModel : CurrencyViewModelType, CurrencyViewModelInput, CurrencyViewModelOutput {
var input: CurrencyViewModelInput { return self }
var output: CurrencyViewModelOutput { return self }
// inputs
var selectCurrency: AnyObserver<Currency>
var reload: AnyObserver<Void>
// outputs
var currencies: Driver<[Currency]>
var showCurrencyDetail: Driver<Currency>
init() {
let currencySubject = PublishSubject<Currency>()
self.selectCurrency = currencySubject.asObserver()
self.showCurrencyDetail = currencySubject.asObservable()
// ...
}
}
Our protocol is finally implemented. However, by implementing each protocol as Input and Output and returning self
, we also expose at the same time all the properties. Nothing is forcing me to use viewModel.input.reload
over viewModel.reload
.
A way to avoid this issue is to redefine the Input protocol to expose functions only and use with private properties.
protocol CurrencyViewModelInput {
var reload: AnyObserver<Void> { get set }
var selectCurrency: AnyObserver<Currency> { get set }
}
struct CurrencyViewModel : CurrencyViewModelType, CurrencyViewModelInput, CurrencyViewModelOutput {
// inputs
private let selectCurrencySubject = PublishSubject<Currency>()
func select(currency: Currency) {
selectCurrencySubject.onNext(currency)
}
private let reloadSubject = PublishSubject<Void>()
func reload() {
reloadSubject.onNext(())
}
...
}
It surely resolve the Input but not accessing Output. At the same time, the use of protocols here didn’t really separate the logic business since all of them are implemented by the ViewModel. It’s all too tied up together.
Let’s see if we can find an alternative.
Nested Structs
I’m going to define a different kind of protocol, I include the Input and Output as associated type to my ViewModel. We’ll be able to define what kind of type we want to associate to our ViewModel.
protocol CurrencyViewModelType {
associatedtype Input
associatedtype Output
var input : Input { get }
var output : Output { get }
}
And here is the ViewModel
final class CurrencyViewModel : CurrencyViewModelType {
var input: CurrencyViewModel.Input
var output: CurrencyViewModel.Output
struct Input {
let selectCurrency: AnyObserver<Currency>
//...
}
struct Output {
let showCurrencyDetail: Driver<Currency>
//...
}
init() {
let currencySubject = PublishSubject<Currency>()
self.input = Input(selectCurrency: currencySubject.asObserver())
self.output = Output(showCurrencyDetail: currencySubject.asObservable())
}
}
What’s great about this solution is that Input and Output are strongly typed with CurrencyViewModel.Input
. It also doesn’t allow an access to the properties without going through Input and Output.
Finally, by keeping the protocol to the ViewModel, we can still inject our component to a View by using CurrencyViewModelType
as well as mock behavior in Unit Test.
In conclusion, we’ve seen how to take advantage of protocol as well as nested struct to structured our ViewModel and get a clean integration of RxSwift in our MVVM architecture. Regardless of the solution you prefer, it should always aim to a clean architecture and code testable.
What about you? What kind is your approach with RxSwift and MVVM?
Happy Coding
Extra resources