Good practices for RxSwift-based view models
Everyone has their own preferred way of structuring their app code. I have grown to like view models and RxSwift. I am not trying to convince anyone to use it with this post, but rather want to share my best practices that took me a few years of using RxSwift to settle on:
- You instantiate the view controller with whatever data model it handles.
- A reasonable complex view controller internally uses a view model, which the view controller instantiates. The view controller does not require whoever is presenting it to know that it’s backed by a view model.
- View models are typically based on RxSwift. They are instantiated with two types of things: The data model and the inputs from the view controller.
- When the view controller instantiates the view model, it passes all the input observables to the view model – usually as a Signal.
- The view model uses the data and the inputs, and transforms them into a number of other output properties that are set during its instantiation: content output that tells the view controller what to display, result outputs that provide the result of any transformations of the data model according to the user’s inputs (or external inputs) including errors that occurred, and navigation outputs that tell the view controller what path to take. The content outputs are typically Drivers, and the result and navigation output are Signal.
- Right after the view controller instantiated the view model, it binds those various view model outputs. View outputs typically are bound to visible UI elements, result outputs show some confirmation or error indications, and navigation outputs lead to presenting different view controller. Only at that time are all of these added to a dispose bag.
This works well in my experience. The code overall is clean and can be nicely encapsulated.
The logic of transforming the data are offloaded to static functions with few inputs and a clear output and no side effects. This helps a lot when writing unit tests for any logic.
I typically end up with fairly lengthy viewDidLoad
methods but they primarily just consist of list of binding the various outputs to the view controller’s views and presentation methods. Similarly, the view models’ init
methods can be long, but are also focusing on transforming and combining the different inputs into outputs, with the actual logic of the transformation being handled separately in static functions that can be nicely grouped into separate extension.
Good indicators whether the separation is properly handled are the following:
- The view controller does not expose its view model nor that its using RxSwift. It is instantiated using the data model and any outputs are exposed using a delegate protocol.
- View models do not have any
var
properties, onlylet
. - View models to not exposé any functions to the view controller, instead all the inputs are provided in the
init
method. However, static functions are fine. - View models do not have a dispose bag. They only transform the data models and input streams to output streams.
- View controllers do not save any state, besides hanging on to the data model used to initially instantiating it, so that they can pass that on to the view model.
- No using of RxSwift’s
Variable
.