Anatomy of an RxSwift View Model
There are many articles and tutorials on how to create view models for use in projects built using RxSwift. I’m adding my own tutorial to the mix because when I started out trying to use RxSwift, I found many of the tutorials hard to approach. MVVM is not complicated and should be easy to implement. My goal with this article is to provide clarity in such a way that those who read this can understand how to create a view model that is easy to create, easy to maintain, and which doesn’t leak memory or resources.
If you are reading this, I assume you know Swift and you’re at least familiar with RxSwift. I also assume you’ve read about MVVM. If not, start with some other tutorials. This tutorial will not explain RxSwift nor teach Swift nor explain MVVM. This is simply about how to construct a good view model.
Why Use MVVM?
In short, MVVM simplifies complex view controllers by separating the “massive view controller” into 2 items, the view controller and the view model, making them both easier to understand and code and maintain. The view controller should focus strictly on UI operations. The view model is where domain model interaction takes place and gets converted into UI inputs. The view controller should not import or access any domain model classes. Likewise, the view model should not import UIKit.
What is a View Model?
I propose a non-scholarly, non-computer science definition. A view model converts Observable UI Inputs into Observable outputs that can drive UI behavior. That is the crux. In doing this conversion, the view model may interact with the domain model and, in doing so, isolate the view controller from being domain aware.
RxExample View Models
The RxExample app includes three example view models designed by Krunoslav Zaher, a key contributor to RxSwift. They are worth studying because they exemplify good view model design. Let’s step through one of them and look at the key aspects of the design. A very clean example is
GithubSignupViewModel2, so let’s start there.
Shape of the view model
Let’s start with the shape of the view model class. It has an init that receives the inputs and dependencies which are used to construct all of the properties that are the outputs of the view model.
Each property is used in the view controller to drive UI behavior. Each input and output in this example is a
Driver, which is highly recommended because
Drivers always run on the main thread, never complete, and never produce error events. That allows the view controller to have very simple responses to those
Connecting the View Model to the View Controller
Take a look at how simple it can be to wire these
Drivers up to the UI controls:
As an aside, there is a much better style permitted for inserting
Disposables into the
DisposeBag in the newer version of RxSwift. Here is what you can do with that same block of code now:
The View Model Initializer
The next thing to note about this view model is the clean way that the inputs and dependencies are passed into the initializer using nicely formatted tuples.
This style leaves no doubt about what the view model’s inputs are and also provides for clear declaration of any dependencies.
In the comments, Zaher points out that in the body of the init method,
Everything is just a definition. Pure transformation of input sequences to output sequences.
This quote sums up the single most important aspect to MVVM view models: transform input sequences into output sequences.
One more thing to notice: there are no other methods on the class. Everything happens in the init function. In contrast, the
SearchResultViewModel is an example of a view model that does have instance methods.
Let’s look at how this affects the design of the init. Because the init method cannot access self until all properties are initialized, and because he wants to use those methods on self to prepare the imageURLs and title outputs, he is forced to give them both a temporary value and then overwrite them with the actual outputs. I don’t recommend this approach.
Avoid Instance Methods on View Model
Instance methods on view models only serve to increase the risk of memory leaks. Any invocation of those methods requires a reference to self. You must be very careful to not use instance mthods inside closures. Zaher does not use them inside any closures and so there is no leak in the
SearchResultViewModel. But I would argue that even using instance methods for this opens the door to unintentional memory leaks. Better to use fileprivate top level functions, as in this refactored version.
Top level functions, as opposed to instance methods, cannot leak self because they have no self. An init with no methods will not accidentally leak self while creating its outputs. In addition, this version has no properties for
$ which appear to be outputs, but are not used in the view controller. They probably should have been marked private.
View Models Should Deallocate After viewDidLoad()
It is also worth noting that when the
GitHubSignupViewController2 reaches the end of
viewDidLoad(), it will deallocate the view model immediately. The view model instance only exists long enough to allow the view controller to bind the view model’s outputs to it’s controls. Don’t believe me? Try adding the following code to
Place a breakpoint on the print statement and run the RxExample-iOS app and choose “GitHub Signup — Using Driver”. When the breakpoint is hit, go up the call stack to
GithubSignupViewController2.viewDidLoad() and you’ll see that the view model is deallocating because the end of
viewDidLoad was reached.
The net result is that
GithubSignupViewModel2.init is acting as a static function. The instance of
GithubSignupViewModel2 that is returned is no longer needed after its outputs have been connected to the controls. The outputs remain in memory until disposed, but the view model instance itself is deallocated. That gives assurance that there is no memory leak.
View Models Can Be An Immutable Struct
GithubSignupViewModel2 could just as easily be a
struct. In fact, change it to be a
struct and it still compiles and runs. Since you only ever need one instance and since all of its properties are immutable, a
struct is actually preferrable for this view model.
View Models Can Be A Func
Since we can and should design the view models to do all of the conversion of inputs into outputs inside of the init method, then we really only need a function. In the gist below I have refactored the view model to be a top level function. No class. No struct. Just a function. Here is the gist which you can paste in the RxExample project to replace the original
GitHubSignupViewController2, then compile and run. No changes need to the view controller.
Key points for a solid view model:
- Inputs clearly identified in the function or init signature
- Transforms input streams into output streams
Drivers are recommended for outputs
- No instance methods — use top level functions instead
- View model should dealloc at the end of view controller’s
- Strongly recommend using a
structor a top level function rather than a class
This article has examples and justifications for the above recommendations. Hope you find it useful and welcome your feedback.