Writing Flexible View Models

Eduardo Domene Junior
The Startup
Published in
11 min readNov 3, 2020

--

Something that I noticed through my career is that developers have different conceptions on how View Models should look like. Usually, they either see it as closer to UI or closer to data. There are both pros and cons for each approach, but I believe that the closer we get to UI and life cycles, more coupled is our code. Bringing View Models closer to data allows us to be more flexible, more expressive, adapt quicker to UI changes, perform AB tests and be ready for new frameworks like SwiftUI.

The terms closer to UI and closer to data used in this article directly relates to how we name our variables and functions in the View Model.

We are going to discuss the points described above, some of which you might have faced yourself when developing applications. For all of them, we will check how those changes impact other parts of the code using the two different approaches. Let’s dive into it.

Scenario 1

When there are changes in the View

Approach: VM closer to Life Cycles

Consider the following example. We have created a ViewModel that semantically references (by name) the UIViewController’s life cycle function viewDidLoad.

This should work well for our application: when we enter the View Controller, data will be fetched as soon as it loads. If we navigate to a different screen from there and then come back to the controller again, data doesn’t need to be fetched once more.

Imagine that a few days later there is a new demand, and now the Product Owner has decided that having this data updated is really important. If you navigate to another screen and come back, this data should be fetched once again.

The solution to this problem could be as easy as moving the place where we fetch data from, right?

Suppose that in order to solve this problem, we decide to change the View Controller so we move our fetching function from viewDidLoad to viewWillAppear. In this case, we would have something like:

That doesn’t look good, does it? Having a .viewDidLoad() inside a viewWillAppear is quite odd. Because of this “semantic coupling”, we are forced to change the name of the function in the ViewModel. On top of that, our test cases should also change to reflect the new name.

In this scenario, if you decide to fetch your data in a different life cycle, that will affect:

  1. The View Controller
  2. The View Model
  3. The Test Cases

Approach: VM closer to data

What if we use a ViewModel which is closer to data, with meaningful names that represent the side effects instead?

Let’s go back to the initial scenario: you need to fetch data as soon as the View Controller loads. However, in this case, instead of having viewDidLoad(), we would have fetchData() in the View Model:

Then, like in the previous example, it was decided that it is crucial that we update the data every time we display the screen. Let’s use the same solution, fetching the data from viewWillAppear instead:

What happened in this case?
We can notice that our program still makes sense semantically. Thus, only the View/ViewController changed, and nothing else.

Because we are not linking the life cycle in the View Model, we don’t need to care about where we fetch our data, it will always make sense to call fetchData in any part of your View Controller.

Impacts of this change:

  1. View Controller

In this scenario, closer to the data approach has less impact. We will have a closer look in UI related names in Scenario 4.

Scenario 2

Changes in View Model

“But what if we want to change what happens once the screen is loaded?”

Let’s slightly change our initial example and check both cases:

Approach: VM closer to UI

Again, let’s assume our initial code, closer to UI, with the addition of a new property called formattedData:

Now, assume that this formattedData needs to include some data which was fetched in fetchData. That’s how it might look like after the new requirement:

Impacts of this change:

  1. View Model
  2. Test Cases

Approach: VM closer to data

Now, let’s assume this initial code for the next example.

If we need to change the behaviour of what happens in viewDidLoad, we will probably need to change both classes:

Impacts of this change:

  1. View Controller
  2. View Model
  3. Test Cases

In this scenario, going closer to the UI is the option with less impact.

Of course we could argue that if we’ve kept the same name for the function fetchData we could’ve avoided the impacts to the other classes. That’s true, but I think the new name fetchFormattedData has more meaning and represents better what is actually happening.

Now you might ask yourself ”what’s the difference then?”

If in the past scenario, the closer to UI option propagated changes to the other classes, and in this scenario, closer to data approach propagated the changes as well, what’s the difference?

It’s common knowledge that if you depend on something, and that something changes, it could affect you. But the opposite is not always the case. For example, when a third party Library in your project changes, you might need to adapt to it. However, if you need something specific for your project, you cannot demand that the Library changes just because of you. The reason is that it serves multiple clients, and your particular case might not fit the other clients needs, or even break them.

In our case, though, we have full control of our dependency, which is in fact something good. Having control of it can help make our code cleaner and more expressive, since we can define what we need and the dependency can adapt to that. At the same time, as closer to our needs the dependency gets, less reusable it is. We will get deeper into that in the next scenarios.

Scenario 3

Multiple actions to perform the same operation

Approach: VM closer to UI

Let’s get back to the first scenario. Suppose that on top of fetching data once the page is loaded, it is also requested that we implement a pull to refresh on the page.

Because you cannot call a viewModel.viewDidLoad() in your pull to refresh action, since the semantics of it would be wrong, you might decide to create a viewModel.pullToRefresh():

Do you see any issues here? I would argue that this is not really scalable.

Since we added another action that may trigger fetching data, we also needed to create one more function in the View Model. Now we have two functions to perform the same operation. What if on top of that, we also want a button to load data once pressed? — Three functions for the same operation.

From the Test Cases perspective, we are simply duplicating the same test. Both viewDidLoad and pullToRefresh perform the exact same operation, but since we cannot test the fetchData function (because it is private), we need to test the publicly accessible methods.

I know that it is a wild example, but what if you have 10 different fetch data actions to perform in the View Controller? Then you have 10 different operations in the View Model that will do the same fetchData(), and 10 test cases that will perform the same validation. If you decide to change the behaviour of the fetchData(), then you have 10 test cases to change the assertions. Of course you could create one assertion method and use it to validate the 10 test cases, but that’s not the point. The point is that you are increasing your test cases with duplicated validations.

Impacts of this change:

  1. View Controller
  2. View Model
  3. Test Cases

Approach: VM closer to data

If we decide that our View Model is closer to the data than it is closer to the UI, we can avoid the semantic coupling from the past example. Again, let’s use fetchData() in our View Model and see what happens:

In this case, it doesn’t matter how many triggers we have in the View Controller to fetch data, the View Model will not be affected. Also, because of that, we don’t need to have the same test multiple times; only one is enough.

Impacts of this change:

  1. View Controller

Some could argue that this View Model doesn’t really represent the View, since it doesn’t contemplate a pullToRefresh action.

My question then would be: what’s more expressive and meaningful for your client (the View)? Suppose that whoever is developing the View does not have any knowledge about the ViewModel. Would he/she know what’s happening behind viewModel.viewDidLoad?

Scenario 4

Adapting to new designs

Designs change. A lot. So it does matter how much impact a new design has in our codebase. Check the following example.

Approach: VM closer to UI

Suppose you develop a very simple Data Monitoring app, with one screen that tells you how much data you spent until this time, and a button which enables/disables the data monitoring:

My UI/UX is very debatable I know; you are totally right! :’(
But let’s stick to it for terms of simplicity.

For this design, lets say we have this (very simplified) code:

Notice how close the View Model is to the View/View Controller. It has explicit name references to the UI components: updateButton and updatePercentageLabel. Also, it formats the dataUsagePercentageLabel, so the View Controller will always receive a value with “%”. It works fine, no problems so far.

A few weeks later, the Design team decides to improve the user experience, and we have to update our app to the new version:

At this moment you realise why I am not a designer! Just pretend that this new design is better than the previous one :)

How can we adapt to the new changes?
Notice that the data is actually the same, only the design has changed. Can we then only make changes to our Views? Let’s see.

Since the button has become a switch, I guess the names updateButton and dataMonitoringButtonTap in our View Model don’t really makes sense now, right? Also, to accommodate the new progress bar introduced in the design, we decide to include it to our View Model as well.

In summary, there are three main changes to the View Model:

  1. Change updateButton to updateSwitch
  2. Change dataMonitoringButtonTap to dataMonitoringSwitchTap
  3. Add updateProgressBar so we can notify the View Controller it needs to change the progress bar component

In the View Controller:

  1. Adapted to new names introduced in View Model.
  2. Added UI code for progress bar.

Approach: VM closer to data

This is how the initial version of our app could be written if our View Model is designed data-oriented:

How is this View Model different from the previous one, in its initial version?

  1. viewDidLoad has become updateData
  2. dataMonitoringButtonTap has become toggleDataMonitoring
  3. updateButton has become isDataMonitoringEnabled
  4. updatePercentageLabel has become dataUsagePercentage

Check how we switched from a UI approach, to a data approach.

Because we rely only on data, it doesn’t really matter if we have a button, a switch, a slider, or whatever other component in the View.

Now, check how it would look like when we implement the new UI provided by the designers:

Actually, the View Model didn’t change at all. Every change was contained in the View/ViewController, which adapted to this new demand using the same data it already had previously.

Compare again the two implementations. Which one has a lower impact in other code?

Some people tend to think that calling an explicit operation from the View Model, like fetchData, is higher coupling than using viewDidLoad or whatever other UI/life cycle related name, but this is not true. They argue that by calling an explicit action, the View Controller knows about the View Model. The fact is that independent on how you name yours methods, the ViewModel is still your dependency. The real coupling happens when your changes affect other areas in your app.

Scenario 5

AB testing

For this one, we will not look at any piece of code. By now you should have noticed how moving into data direction can make our code more flexible. Take for example the last scenario, where we had two different designs using the same data. The View Model which was closer to data let us adapt better to UI changes, but there’s another advantage to this approach: we can actually unlock easy AB testing. We could have used, for instance, the two different views from the past scenario to check which design is better suited for our customers. A control group would get the “original” version, while another group would see the new version, both using the same View Model.

Scenario 6

Migrating to a new UI framework

Finally, let’s briefly talk about SwiftUI. It’s a very new framework, and probably most apps will need some time to make the necessary changes to use it. Based on the past scenarios that we have discussed so far, which approach is friendlier for moving away from UIKit and migrating to SwiftUI? Does SwiftUI have the life cycle methods from UIKit? No. The closest it has are onAppear() and onDisappear(), but that’s it. Why would we couple our View Models with life cycles that are totally dependant on the UI framework then?

If you have read until here, thank you! I hope you find this discussion useful and that you can take something from it.

Additional thanks to Mario Rodrigues and Michel Ribeiro for reviewing this article and discussing some of the topics with me.

--

--