Dealing with Massive View Models using MVVM on iOS

Four practices that will help you to get rid of Massive View Models

Pablo Manuelli
etermax technology
9 min readJul 30, 2019

--

It is no secret that Apple’s Model-View-Controller pattern tends to have a little problem called Massive View Controllers. So, as an alternative to MVC, some of us use the Model-View-ViewModel pattern instead. At first it looked like MVVM was the solution for this problem. And, on some level, this was true: We no longer have to deal with Massive View Controllers, but what we have now are Massive View Models.

Where did it all go wrong?

Maybe we forgot why we didn’t like Massive View Controllers in the first place. Besides having all kind of responsibilities, Massive View Controllers are really difficult or impossible to be tested. And, no surprise, Massive View Models have exactly the same problem, even if we don’t have to worry about the view any more.

So, what can we do to avoid Massive View Models? I have a couple of advices that might help. But first, let’s do a fast recap about MVC and MVVM.

MVC

Model-View-Controller is the suggested architectural pattern by Apple for iOS development. Here, instances of UIViewController serve as Controllers and instances of UIView as Views. In this particular version of MVC the Controller depends directly on UIKit.

Model-View-Controller pattern

If we want to test a UIViewController then we need to instantiate the view. Usually this is not so simple and makes our tests run slower. Another option is to try to mock the view, but this isn’t easy either.

Controllers are hard to test for this dependency with UIKit and the multiple responsibilities they tend to have.

MVVM

This is the architecture that I use as an alternative to MVC. In this pattern, instances of UIView or UIViewController represents the View. The ViewModel doesn’t know the View and is independent of UIKit.

Model-View-ViewModel pattern

Under this scheme, the ViewModel is easier to test. We no longer need to instantiate the View, so our tests are easier to write and even run faster.

But what kind of responsibilities does the ViewModel have?

Massive View Models

If we are not careful, all this responsibilities that used to belong to the ViewController now belongs to a ViewModel. We will end up with a ViewModel that does almost the same things that the ViewController once did: transform Model elements to be presented in the View, update that Model, perform networking calls, decide which screen comes next, etc.

We didn’t win much if instead of having Massive View Controllers now we have Massive View Models, right? The application is still difficult to test.

To make our code more testable we need something more. A different architectural pattern is not enough. We need some good practices of software development like the SOLID principles, Clean Architecture, Test Driven Development, Domain Driven Design, among others. Following these practices we will have low coupled and high cohesive classes, which are easier to test, and a flexible code base.

How to deal with Massive View Models

Next I will give you some advices and explain some components that I use to avoid Massive View Models. These elements come from applying this good practices that I mentioned earlier.

Well, enough talk. Let’s dive in.

1. Sub View Models

We do not need to move all the ViewController’s behavior to just one ViewModel. If a ViewController is complex (it contains many different elements or provides many interactions) then it’s ViewModel will be complex too.

Following the Single Responsibility Principle we can create more than one ViewModel for a single ViewController. And with the collaboration of all of them fulfill the expected behavior.

Single Responsibility Principle: A class should have only one reason to change.

To achieve this our main View can be divided into several different components, each with their own View and ViewModel. This is what we already do with the cells of a table: We create a CellViewModel and instantiate one for every cell. But we should not limit ourselves to doing this with cells only. Different cohesive parts of our main View can be extracted into new Views, each one with its own ViewModel. This way, smaller ViewModels are created, with fewer responsibilities and easier to test.

But be pragmatic. You don’t need a ViewModel for every UIView element. Start with one ViewModel and when it grows too much (if it does) then look for some new ViewModel that can be extracted. You can always find one.

2. Dependency injection

Dependency Injection increases considerably our ViewModel’s testability. With this technique any collaborator that our ViewModel interacts with, like actions or services, should not be instantiated inside our class. This instances should be provided by another object. For example our ViewModel can obtain its dependences in the constructor.

This technique increases not only our ViewModels’ testability but our entire code. I recommend using dependency injection in all our classes.

In Swift, to inject dependences you should use protocols. Rather than depend on concrete classes, you must use protocols to define dependencies. This allows us to inject different Test Doubles in our tests that will help testing the ViewModel.

Let’s see an example. We have a LogInViewModel that uses a LogIn action to perform… a log in.

LogInViewModel without Dependency Injection

How can we test the behavior of the ViewModel when the action succeeds or fails if we do not control the conditions that make this action succeed or fail? Well, if we own the LogIn action we can go and check what this conditions are and try to reproduce then in the test. But what are the disadvantages of doing that?

  • This conditions can be difficult to reproduce on a test.
  • If the conditions change in the future our test will break and we must fix it.

What if we use some dependency injection here?

This is the same example but the LogIn action is injected in the ViewModel’s constructor. Note that in this case LogIn is not a concrete class but an interface.

LogInViewModel with Dependency Injection

Why LogInViewModel is easier to test now?

  • Now we can use a test double (a stub for example) to control success or failure of the LogIn action.
  • If the productive class that implements LogIn changes, our ViewModel tests do not need to be fixed.

Dependency injection is a technique that complies with the Dependency Inversion Principle.

Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.

3. Actions

In the previous example LogInViewModel interacts with a LogIn action. But what is an action?

The concept of Action that I use is pretty similar to an Application Service on Domain Driven Design (do not confuse with a Domain Service, you can find the differences between them here). In Clean Architecture they are called Use Cases.

Actions represent the ways that the user interacts with our system, like performing a log in. This actions interact with our domain or the infrastructure (or both) to fulfill the use cases. They represent the entry points to the domain. I like to keep them agnostic about the view: this is a ViewModel’s responsibility.

This is what works for me, but feel free to use the definition or implementation that suits you best. What is important here is to keep this logic outside our ViewModel.

And don’t forget to use dependency injection with your Actions and test them!

4. Coordinators

Where do you manage the navigation flow of your application? Have you ever tried to reuse a UIViewController somewhere else in your application and it’s been painful to deal with the navigation?

Coordinators can help you with that. This objects decide what is the next screen in your application when users touch some button. They deal with the details of doing it using UIKit: they know how and when to perform a push, pop, present a modal or dismiss it.

Remember that dependency injection thing that we talked about earlier? Coordinators can be used to pass dependencies down the application.

Let’s add a coordinator to our previous log in example and try to explain better what coordinators do:

Let me explain step by step what is going on here:

  1. The coordinator receives in its constructor a reference to a UINavigationController that will be used to show whatever is needed. In this case we will push a LogInViewController. Does this sounds familiar? Yes! More dependency injection!
  2. The coordinator chooses which implementation of the LogIn protocol should be used. Remember that LogInViewModel depends on a protocol, not on a concrete class. In this case the implementation chosen is the DefaultLogIn class (more on this name later). Couldn’t we have received this instance by constructor like we received the UINavigationController? Yes, we could have, but at some point someone have to instantiate the concrete class. I’m OK with the fact that coordinators create some dependences that need to be passed down to the ViewModels, but if it’s convenient to receive them in the constructor I would do that.
  3. Here the ViewModel and the ViewController are created. Note that, as in any good MVVM implementation, the ViewController receives the ViewModel in its constructor. Also note that the LogIn instance is passed to the LogInViewModel.
  4. Finally the coordinator pushes the instantiated ViewController using the navigation’s instance received as a dependency in point 1 and the LogInViewController appears in screen.

That’s great!

But… What happens when the user completes the log in? Who decides where to go next? The coordinator, of course. We can introduce some changes here to make that happen:

Lets see the changes that we have made here:

  1. We connected the log in completion with the startDashboardCoordinator method using a home-made binding (more on why I did it that way later). Keep in mind that logInCompleted block is executed by the LogInViewModel when the log in succeeds.
  2. When the startDashboardCoordinator method gets called then the coordinator hands on control to the next coordinator in the flow. A DashboardCoordinator is instantiated and started.
  3. We need to maintain a reference to the instantiated DashboardCoordinator to prevent it from being deallocated.

The LogInViewModel doesn’t know what screen will come next when the log in completes. And it shouldn’t. Its job ends when the logInCompleted block is called.

Coordinators reacts to events in the ViewModel (for example, a button that is pushed or an action that completes) and do what is needed when the event is received. That could be show a new ViewController on screen or hand on control to another coordinator. That way the ViewModels are decoupled from the navigation flow and could be reused somewhere else if needed. It can be reused even in different coordinators.

This is just a simple example. If you want to know more about coordinators you can find more details here and here too.

About the home-made binding

If you are familiar with RxSwift (and maybe you are if you work with MVVM) that home-made binding may seem unnecessary. That’s true, in a real world application I would have used RxSwift to do the binding. I intentionally didn’t do that way to keep the complexity of RxSwift out of the example.

About the DefaultLogIn class name

I want to take a moment to explain why did I choose that name for the LogIn protocol’s implementation. This is just a convention that I like to follow.

When I introduce a protocol just to make a class testable, this usually means I end up with only one class implementing this interface in production code. In these cases I like to name that class as Default (DefaultLogIn in this example).

If the time came that I needed two or more different implementations then I would rename the DefaultLogIn class to denote what makes this implementation different from the others. And of course name the other classes accordingly. For example if one login action uses an email and the other is through Facebook, the names could be EmailLogIn and FacebookLogIn respectively.

Conclusion

We have covered a lot here.

I’ve showed you some concepts and ideas that can help you get rid of Massive View Models. I’ve been working this way for a couple of years and I totally recommend it. Give them a try. Maybe you can start applying one or two.

You’ll see that your ViewModels are going to be smaller and more organized, with fewer responsibilities and easier to test.

And a good thing about testing is that you can change your code with more confidence, because tests help you realize when a bug has been introduced.

This leads to spend less time on debugging. And less time on debugging leads to a happier life.

Do you want a happier life, don’t you? Start by making those ViewModels smaller!

Thanks for reading! 😄

--

--

Pablo Manuelli
etermax technology

Principal iOS Software Engineer @ Trivia Crack — Etermax 🍎