Modeling Your View Models as Functions
Note: this article assumes that you know the basics of RxSwift or have a general understanding of MVVM. If you don’t, there are a ton of incredible resources (including the ones linked just below) on the Internet to help you get started.
Another note: My eyes were initially opened to this idea by Brandon Williams and Stephen Celis of Point-Free. If you haven’t checked them out you should. The content they put out is absolutely indispensable for Swift developers and a subscription may be the most valuable investment you make this year.
Intro
Thousands of words have been written and hours of talks have been given about how to pair RxSwift with MVVM. At Grailed we’ve always been keen on innovating with the community with the motivation to improve our code and create a better, more reliable product for our consumers. With this goal in mind, we have been using a form of MVVM that looks to functional programming and RxSwift to provide reliability, testability, and stability. We love many aspects of it but have encountered a set of problems with which most MVVM developers will be familiar.
The Problems
It can sometimes be unclear how to structure your code. In the dozens of variants of MVVM floating around the internet, every one seems to have a slightly different take on how the view layer should interact with its view model. This lack of a clear pattern can make developing in MVVM feel ad hoc and inconsistent, which can lead to maintainability problems.
View models can also become incredibly verbose. In response to the lack of structure in many flavors of MVVM, others have opted to write a much more explicit contract of inputs and outputs to the view model, but traditionally this has come at the cost of a hefty dose of boilerplate.
Meanwhile, other versions don’t prevent the consumers of a view model from incorrectly using its API. Subscribing to a view model’s inputs from your view layer is well-known to be an anti-pattern, but it’s something I’ve seen in multiple production codebases. Ideally the compiler would prevent us from making this mistake altogether.
View models can also become hard to set up because of Swift’s class and struct initialization rules. For example, when you have to set output Observable
s as properties on your class or struct, those outputs depend on input Subject
s. You can sometimes encounter a situation where you can’t reference self
until you finish initializing all of your properties, but you can’t initialize your properties because you need to reference self
.
It’s also easy to forget to bind your view model’s inputs or outputs and the compiler won’t help us figure out when we’re doing it wrong. The compiler is our friend and it would be great if it could help us out so we don’t make such silly mistakes.
One Solution
Now that we’ve gone over some of the pain points of MVVM with RxSwift, let’s take a simple code example written in one popular style of MVVM and look at how we can improve it.
It is well known to developers of all strokes that writing pure functions can unlock levels of testability and understandability that would otherwise be difficult, if not impossible, to achieve. The problem is that many types of code that we write do not fit neatly into pure functions so we strive to model as many parts of our code as pure functions as possible. This insight has driven many MVVM developers to create their view models to have an explicit set of inputs and outputs so that we can treat them more akin to functions.
The inputs are function calls or Subject
s, the outputs are callbacks, mutable variables, or Observable
s, and the business logic of a view is modeled inside the view model as transformations of inputs to outputs, mostly in the view model’s initializer. Probably the first and definitely the most well-known iteration of this is the Kickstarter open source app. Even though it was written in ReactiveSwift, the ideas are very much the same. This app being open sourced opened many developers’ eyes (including my own) about how we could use MVVM to achieve testability and stability in our applications.
Lets take a classic RxSwift example, a simple login form with username and password fields, as well as a login button. We only want the button enabled when both the username and password are filled out, and when the user hits the button we want to display some sort of message about their successful login (we will hard-code it here and in future articles go over how to handle networking). Here is an example of how we might write this if we are following the pattern laid out in the Kickstarter application.
There’s a lot going on here for such a small screen so take a minute to digest it. There are a few things I really like about this style:
- The view model creates a very explicit contract about what it is capable of and how it should be used
- There are very few ways to incorrectly consume the view model’s API
- The view model has no side-effects
- It’s very easy to look at this view model from the outside and understand how to test it
Despite these benefits, it has a few fairly significant drawbacks:
- To achieve the explicit contract we enjoy so much we have to write an inordinate amount of boilerplate: two protocols that duplicate the view model’s interface, a protocol that merely erases the interface to expose inputs and outputs, and a bunch of private properties and public interfaces to them
- The signal to noise ratio is very low, which makes it harder to decipher exactly which parts we care about when we come back to read this in the future (our business logic lives almost entirely in the view model’s
init
) - In practice it’s easy to forget to subscribe to one of the bindings, which can slow down development and even cause production bugs
Overall I think this tradeoff is a worthwhile one. We’ve invested in a little extra boilerplate and some messiness inside our view model in return for a very explicit and easy-to-reason-about external API with a clear idea of what the view model is capable of. This ease of reasoning allows us to think about the view model almost as an abstract form of a pure function, where we pass in inputs and receive outputs.
Because these inputs are generally user actions and the outputs are side effects, it also gives you the benefit of being able to write higher-level tests where you walk through a series of user actions/inputs and ensure that the side effect-causing actions trigger in the way you expect. This allows you to write a broader set of high-level “feature” tests that test the app how the user would experience it in addition to more traditional unit tests.
An Evolution
So we’ve improved the testability of our code and provided a safe and understandable API for our view models, but it still leaves us wanting more. Can we eliminate much or all of the boilerplate without sacrificing explicitness and safety?
You might (not) be shocked to hear that the answer is yes! To think about this solution it’s worth going back to first principles of this style. We want to treat our view models as much like pure functions as we can, with explicit inputs and outputs that we can easily test for correctness. There is a simple, low-cost construct in Swift that takes inputs and returns outputs: functions. So can we use functions as our view models rather than classes or structs? Lets think about how we might be able to do that.
Rather than a LoginViewModelInputs
protocol, we can just pass our inputs in as parameters of our function, and, rather than a LoginViewModelOutputs
protocol, we can just return our set of output Observable
s from our function. It sounds radical, but lets take a look at our example to see what this might look like.
Our view model has magically transformed into a function, and now inside our view controller instead of calling LoginViewModel.init()
we instead call loginViewModel(usernameChanged:passwordChanged:loginTapped:)
. Let’s take a look at some of the improvements we’ve made:
- We have completely eliminated our input and output protocols (and therefore much of our boilerplate) in favor of function arguments as inputs and a named-argument tuple as an output
- We no longer have to bridge our inputs to
Observable
s since we are givenObservable
s directly by our view layer, eliminating even more boilerplate - We are doing less binding/subscribing in our view controller, which has significantly improved the signal/noise ratio
- If we forget to pass an input into our view model we will get a compiler error because we must provide all arguments to call a Swift function
- If we forget to use an output, we will now receive an “unused variable” compiler warning due to the fact that we are destructuring our output tuple
- If we reuse a view model across different screens I as a developer can explicitly choose to ignore a specific output using an
_
, which makes it clear to reviewers and future maintainers that this was an intentional omission, rather than something that we forgot to bind. - We no longer have to dance around struct and class initialization rules because we are no longer using structs or classes!
- We have shrunk our overall implementation by ~30%, and our view model size by ~50%
Once we are able to get past the initial strangeness of not using an object, this is a huge improvement in basically every measurable way. It’s a significant step in the direction of having the compiler remind us when we are making a mistake, and we have vanished most of our boilerplate.
Conclusion
It might seem crazy at first to represent our view models as functions, but we write chunks of business logic as functions all the time in our apps, and what else is a view model if not a chunk of business logic? When we set aside our traditional conceptions of what view models are and embrace our good friend function, we discover a simple and elegant solution to many of the woes of writing reactive MVVM.
The proto-version of this style came from the Kickstarter pagination logic, which has been in production for 3+ years and has been reused in nearly a dozen places throughout their codebase. As this demonstrates, this approach isn’t specific to view models, and it is battle-tested in large scale production codebases, not just something for toy apps like our login screen.
There is a common pattern in the Swift and other communities of creating an object that encapsulates some data and provides accessors to that data. Whenever we see this we can use the same transformation we did with our view model and convert it to a function.
In followup articles I will discuss how we incorporate networking and side effects into this structure, what to do if our view models get too big, how we test our view models, as well as how we’re leveraging this and other shared architectural patterns across both iOS (Swift) and Android (Kotlin) to enable our mobile developers to work cross-platform.
In case you don’t have the patience to wait for those articles to come out, Stephen Celis and Danny Hertz both gave talks recently at the superb Functional Swift Conference about this pattern that are worth watching if you want to see a slightly more complex example of this in action.
If you found any of this interesting, you might also find working with us interesting! Grailed is based in NYC and we’re hiring! To keep up to date with the next posts in this series follow me on Twitter.
I have created a simple demo project that demonstrates the login screen implementation we’ve discussed here.
Thanks to Shai Mishali for helping me get started writing this article, for his work in maintaining RxSwift and related repos, as well as for letting me steal some of his RxSwift/MVVM example code.