Brigade’s Experience Using an MVC Alternative

Ryan Quan
Brigade Engineering
14 min readSep 15, 2014

--

VIPER architecture for iOS applications

Any iOS developer will tell you that iOS apps are built upon the Model-View-Controller (MVC) design pattern. They might also tell you that some like to call it the “Massive View Controller” design pattern. Reason being, you’ll often find you have code that clearly doesn’t fit in the view or model, so it gets chucked into the controller, resulting in a bloated controller that is difficult to maintain because you have one giant class doing everything.

In developing Brigade’s iOS app, we wanted to architect it in such a way that cleanly addressed a few key points:

  • Easy to iterate on
  • Collaboration-friendly
  • Separated concerns
  • Easy to test

We stumbled upon an architecture called VIPER, which seemed to fit the bill. VIPER was developed by the guys over at Mutual Mobile and they have a nice introductory blog post as well as a more involved blog post. We decided to stray from the Apple standard and give VIPER a shot.

What is VIPER and how are we using it?

As stated by its creators, VIPER provides a guide to building an application architecture and can be adjusted to fit individual applications. This will be a discussion of what VIPER is and how we’ve adjusted it to fit our needs.

To best understand VIPER, forget everything you know about MVC. VIPER is a new kind of beast (or reptile) and if your mindset is still in MVC land, you’ll have a difficult time accepting VIPER. Imagine that you are starting from square one and have no idea how iOS apps are structured. There is no spoon. There is no MVC.

VIPER aims to separate out application concerns into one of several roles:

  • View/User Interface
  • Interactor
  • Presenter/Event Handler
  • Entity
  • Router/Wireframe

Note: Some of these have alternative names that you’ll see if you look at the other VIPER blog posts referenced above. I’ve added both names here as a heads up.

In practice, we’ve found that we use a few additional roles, adding on a data-manager and service layer.

Below is a diagram of a typical VIPER “stack” (more on this later) in our app and depicts how we think of VIPER. Each box represents a separate class and each line represents a reference between instances of the two respective classes.

Think of each of these classes as a worker in an assembly line. Each class only knows how to carry out a limited set of actions and relies on other classes to aid in completing the task at hand.

Let’s first go over the responsibilities of each class, then we’ll give an example of going from user interaction to displaying data to the user.

View/User Interface

View responsibilities:

  • Display information to the user
  • Detect user interaction

The view is told by the presenter what to display and it tells the presenter when an event needs to happen.

Displaying information to the user

If the view should display an error to the user, the presenter might call a method on the view like:

It is then up to the view to display the error as it pleases, which could be in an alert view, a label, or any other way. The key thing here is that the presenter isn’t concerned with the details of how the error gets displayed. It just cares that it gets displayed.

Detecting user interaction

If an event occurs such as the user tapping on a “Log in” button, the view might call a method on the presenter like:

As we had before, the object making the method call just hands off the task to the next worker in line once it’s completed its job. As far as the view is concerned, it completed its job to detect the user interaction and it is now up to the presenter to deal with the event.

Presenter/Event handler

Presenter responsibilities:

  • Tell the view what to display
  • Handle events

The presenter tells the view what to display and it handles events accordingly.

Telling the view what to display

As we just saw, one of the presenter’s duties is to tell the view what to display. To accomplish this task, it also acts as a decorator, or a “presenter”, to format the data for the view so that it can be displayed in a meaningful way.

Looking back at our previous error example, the presenter likely received the error in the form of an error object. Instead of handing off the error directly to the view, it determined the appropriate message to describe the error and handed that off to the view.

Handling events

The presenter usually receives event type method calls from two sources: the view and the interactor.

View events

The presenter is notified of events by the view and part of its job is to handle those events accordingly. This usually means asking the interactor to retrieve some bit of information or carry out some task.

In our log in example, the presenter would have been notified by the view that a log in attempt event had occurred with a specific user name and password. The presenter would then ask the interactor to carry out the attempt by calling the appropriate method on it:

You can probably guess what the presenter does now; Nothing. It has carried out its duty of handling the event and it is now up to the interactor to carry on the task.

Interactor events

The presenter can also receive events from the interactor. This would happen when the interactor finishes carrying out some task and the user should know about the result.

For example, if a log in attempt failed, the interactor might tell the presenter:

The presenter would then take this error, convert it into a string that is meaningful for the user, then tell the view to display an error using that string.

Interactor

Interactor responsibilities:

  • Perform business logic

The interactor is what performs the business logic of the app that revolves around data.

Performing business logic

The interactor is the one that knows how to carry out the events that the view notifies the presenter of.

For example, suppose that in your app some event occurs that requires two synchronous network requests to your backend API. That is, request B cannot be executed until request A finishes because request B requires information that is in the response of request A. This would start out with a call to the interactor from the presenter:

One thing we must bring up here is that the interactor itself does not deal with network requests directly. In fact, it doesn’t even know that network requests are occurring. All it knows is that it can get data in the form of entities from the data manager (we’ll elaborate on this concept soon). With that in mind, the interactor’s corresponding method to the call above would look something like this:

Here we see that the interactor calls the necessary methods on the data manager and then calls back to the presenter with the result. The key thing here is that the interactor knows that `performMyTask` requires both `fetchFooWithCallback:` and `fetchBarWithFoo:callback:` to be called AND that `fetchBarWithFoo:callback:` must be called within the callback block of `fetchFooWithCallback`. In this way, the interactor handles the “business logic” of the app.

Data Manager

Data manager responsibilities:

  • Retrieve data
  • Store data (optional)

The data manager is the one that knows where to retrieve data from and if it should be persisted or not.

Retrieving data

As we saw in the interactor example, we should be able to query the data manager for data and just get the correct entities back. We don’t care where the data comes from because the data manager handles that.

The data manager knows exactly where to retrieve specific data or carry out certain requests. For example, the interactor might request a user object from the data manager:

If this is an app that is backed by a backend server, the data manager would know that it has to eventually make a network request to retrieve user data. If this is an app that is not backed by a backend server, the data manager might retrieve the user data from a local persistent store instead. For network requests, we like to have the data manager query Service objects:

We’ll get into Service objects soon, but the key here is that the data manager knows what service to use to retrieve specific information. Once that information is received, it relays the information back to the interactor.

Storing data

Persisting data is also concern of the data manager. It knows when it should store data such as data fetched from the server that it might store for caching purposes. The main thing here is that no other class knows that any data is persisted because the data manager abstracts that away. A simple example of this is a modification of the code above:

Let’s break this down:

  • The data manager first checks if it has a cached version of the user corresponding to the given `userID`.
  • If the user is found, the interactor is notified and the method returns.
  • If the user is not found, the data manager must retrieve the user from the backend server and it uses a Service to do so.
  • Once the user is retrieved from the server, the user is first persistent, then the interactor is notified.

Persisting data is a huge topic all on its own and can be implemented many ways. This example just serves as a simple instructional schema, but the idea of abstracting all of this behind a data manager is one of the points of VIPER.

Service

Service responsibilities:

  • Execute network requests to the server for specific entities

Service objects are not necessary in VIPER, but one that we have found very useful.

Executing network requests to the server for specific entities

We’ve found Services to be a nice way to keep code concerned with network requests DRY. The idea is that a single Service only deals with one entity type and knows what network requests need to be made to perform various modifications to that entity type.

For example, you may have a Service for a user entity. The header file for this class might look like the following:

This service knows how to create a user, log a user in, and fetch a user object. Something like fetching a user object for a given ID is something that is likely to be used in multiple places in an app, which is where service objects come in handy in keeping things DRY.

Entity

Entity responsibilities:

  • Represent data

Entities are pretty straight forward and what you would expect. They embody some type of data and act as the “payload” that gets passed around between the other classes. For example, the data manager returns an entity to the interactor, which returns an entity to the presenter, which then uses that entity to tell the view what it should display.

Router/Wireframe

Wireframe responsibilities:

  • Initializes all the other classes
  • Handles routing to other views in the app

The router/wireframe is what glues all of the other VIPER components to one another and handles navigating from one view to another in the app.

Initializing all the other classes

You might be wondering how all of these VIPER classes get instantiated and wired up to talk to one another. That’s where the router/wireframe comes in.

In VIPER, every “stack” consists of a view, presenter, interactor, data manager and services (for us anyways), and entities. This “stack” is what VIPER refers to as a module. A VIPER module should correspond to a single use case. A use case is some function your app should perform for the user.

For example, one use case that is common for many apps is allowing the user to log in with an account. For this, we would have a VIPER module specifically for the “Log in” screen of the app. This module would have:

  • A view that displays the “Log in” screen.
  • A presenter that handles events like the user requesting to log in with a specific username and password.
  • An interactor that knows what methods to call on the data manager for such events like attempting to log the user in.
  • A data manager that knows what services to use to retrieve or send information to the server.
  • Services that know what HTTP URLs to make requests to.
  • Entities that your server responses are converted into so that the information is useful in your app.

As you’ll notice, the view, presenter, interactor, and data manager are very specific to this module. That is, they only know how to deal with things related to logging in. The services and entities on the other hand are very general things that can be used throughout many different modules. In this case, the service might be one that knows how to hit all the endpoints related to Users on your server. This might include logging in, signing up, or just retrieving general User data. Then entity you might use here would be a User entity, which just represents a User object. It’s easy to see that this entity can be used in many places in your app.

Now that we’ve explained what a VIPER “stack” or module is, we can explain part of the responsibility of the wireframe. The wireframe is what instantiates instances of each of these VIPER components and wires them up to talk to each other. That is, it gives the view and presenter references to one another, the presenter and interactor references to one another, and so on. Instantiating a wireframe is equivalent to instantiating an entire VIPER module.

Handling routing to other views in the app

The wireframe’s other alias, router, is what makes up the R in VIPER. The wireframe knows how to navigate to and present other modules when requested. This means the wireframe will also have references to other wireframes.

For example, suppose you have an initial screen in your app that is your typical home screen for an app that requires user accounts. This screen would have a button for “Sign up” and a button for “Log in”. This screen is a module; let’s call it the registration prompt (naming is hard). The use case here is that the user should be able to see their options before being logged in to your app. When the user presses the “Log in” button, the typical flow is:

  • The view will notify the presenter that the user has requested the log in prompt.
  • The presenter will realize this requires a separate module so it will notify the wireframe.

Now, the registration wireframe will instantiate the log in wireframe, thereby instantiating an entire VIPER module, and present the log in view over the registration view (maybe as a modal).

Benefits of VIPER

After using VIPER, we’ve found it to be very beneficial in many ways. Let’s get back to the list of things we set out to accomplish when architecting our app to see if VIPER addresses them.

  • Easy to iterate on
  • Collaboration friendly
  • Separated out concerns
  • Spec-ability

Easy to iterate on

One thing that is very helpful in adding features to an app is knowing where new code should live in relation to the old code. VIPER does a good job of this with each of its components having a clear set of responsibilities that doesn’t bleed into one another, which makes it easy to decide where to put new code.

I often find that when I’m adding new features to a module, it almost feels like an everyday routine because I already know where each piece of code should be placed and it’s just a matter of getting it done.

That being said, we do run into situations in VIPER where we’re unsure of where to place some piece of code and we have to make a judgement call. For example, if the user can select multiple items in a list before performing some action on them all, should we keep this state in the view, presenter, or another other class in the VIPER stack? Once you figure out what makes most sense to you, you can bypass these problems in the future easily because you’ve solved them already. Then everything becomes routine again.

Collaboration-friendly

VIPER makes working in a team surprisingly easy. Because use cases are separated into different modules, you don’t step on other people’s toes since all the code for one feature is usually compartmentalized into its own module.

In practice each of the VIPER components interact with one another via an interface. In objective-C this is just a protocol. The nice thing about this is that you can define the interface between two classes and then separate people can work on those classes individually. We’ve had much success with this by defining the view-presenter interface ahead of time and having one person work on the view doing purely UI work and another person work on the rest of the VIPER stack doing purely “backend” work.

Separated concerns

With each VIPER module abiding by the Single Responsibility Principle, VIPER naturally separates out concerns among classes. This is what makes the former two points (and the last point) work out so nicely.

Easy to test

By having separate components that follow the Single Responsibility Principle, this also makes things easy to spec. It gives you the ability to spec specific functionality while stubbing out other dependencies. For example, if you want to test your interactor logic, all you have to do is stub out the presenter and the data manager to eliminate the dependencies that can make writing specs complicated. Then your specs will only be testing the interactor and you can write separate specs for the presenter and data manager.

Conclusion

Overall we’ve found making the switch to VIPER to be very helpful and beneficial to us for the reasons mentioned above. There are of course many obstacles you will encounter in using VIPER that will require you to mold VIPER to your needs, but that is true of any architecture you choose to go with.

I would definitely recommend others to try out VIPER and also to take a look at the other blog posts on it. Hopefully this post has been helpful and gotten some of you excited to try out VIPER.

--

--