This blog post is an authorized translation of Goksel Koksal’s Blurring the Lines Between MVVM and VIPER. At the end of this blog post, I put my perspective on business architecture patterns. Here’s the translation:

If you’ve ever developed a mobile App, you’ve probably heard of MVVM and VIPER. While there are arguments that MVVM doesn’t scale well enough, there are also arguments that VIPER is a product of overdesign. And what I’m trying to say here is that they’re so close that we don’t even have to separate them.

Let’s take a quick look at MVVM and VIPER.

What is MVVM?

  • The View passes user behavior to the View Model.
  • The View Model processes these behaviors and updates their state.
  • View modelThen informviewThis step can be passedData bindingordelegationandblocksThe implementation.
What is VIPER?

  • The View passes user behavior to presenter.
  • Presenter passes these behaviors to the Interactor or router.
  • If the behavior requires computation, the interactor handles it and returns the state to presenter.
  • Presenter converts this state into presentation data and updates the view.
  • The Router encapsulates the navigation logic that is triggered by presenter.

To learn more about these two architectures, check out this awesome article Bohdan Orlov: iOS Architecture Patterns*

What are our main objectives?

The primary goal is to separate the UI from the business logic. This allows you to update the UI without breaking any of the business logic, or to test the code of the business logic separately. In fact, both MVVM and VIPER can achieve this goal, just in different ways. From this point of view, they could look something like this:

A fictional App: TopMovies

Let’s say we want to use MVVM to make a simple App that pulls down the TOP 25 movies on IMDB and displays them in a list. The component code would look something like this:

protocol MovieListView: MovieListViewModelDelegate {
  private var viewModel: MovieListViewModel
  func updateWithMovies(_ movies: [Movie])
  func didTapOnReload(a)
  func didTapOnMovie(at index: Int)
  func showDetailView(for movie: Movie)
}

protocol MovieListViewModelDelegate: class {
  func viewModelDidUpdate(_ model: MovieListViewModel)
}

protocol MovieListViewModel {
  weak var delegate: MovieListViewModelDelegate? { get set }
  var movies: [Movie] { get }
  func fetchMovies(a)
}
Copy the code
The data flow:
  • The View acts as a delegate for the View Model.
  • The user clicks and reloads.
  • View calls the View ModelfetchMoviesMethods.
  • When the data is successfully fetched, the View Model notifies the delegate(View).
  • callupdateWithMoviesAnd turn the movie object into display data to display on the list.

Pretty simple logic, right? Next we created a basically identical App on macOS and reused as much code as possible.

Hypothetical scenario: Implement the macOS version

One thing is for sure, the view class is definitely different. So we can’t reuse the code that shows the logic in the iOS App. The iOS view has already converted the movie object to display data in updateWithMovies, so the only way to reuse this logic is to pull it out. We moved the code that created the demo data into an intermediate class between the View and the View Model, so that we could reuse this code in iOS and macOS views. So we call this intermediate class Presenter, a name that has nothing to do with The VIPER

protocol MovieListView: MovieListPresenterDelegate {
  private var presenter: MovieListPresenter
  func didTapOnReload(a)
  func didTapOnMovie(at index: Int)
  func showDetailView(for movie: Movie)
}

protocol MovieListPresenterDelegate {
  func updateWithMoviePresentations(_ movies: [MoviePresentation])
}

protocol MovieListPresenter: MovieListViewModelDelegate {
  private var viewModel: MovieListViewModel
  func reload(a)
  func presentation(from movie: Movie) -> MoviePresentation
}

protocol MovieListViewModelDelegate: class {
  func viewModelDidUpdate(_ model: MovieListViewModel)
}

protocol MovieListViewModel {
  weak var delegate: MovieListViewModelDelegate? { get set }
  var movies: [Movie] { get }
  func fetchMovies(a)
}
Copy the code
The data flow:
  • A View acts as a delegate for a Presenter.
  • Presenter acts as a view Model delegate.
  • The user clicks and reloads.
  • View calls presenter’sreloadMethods.
  • Presenter calls view ModelfetchMoviesMethods.
  • When the data is successfully fetched, the View Model notifies the delegate(Presenter).
  • callupdateWithMoviesConvert the movie object to data for presentation and notify the delegate(View).
  • View updates itself.

This means that we can implement this requirement across platforms by making any view comply with the MovieListView protocol. Now we have implemented the new macOS App by reusing most of the code from the iOS project. But then Apple made a big announcement…

Hypothetical scenario: iOS redesign

MovieListView

protocol MovieListView: MovieListPresenterDelegate {...func didTapOnMovie(at index: Int)
  func showDetailView(for movie: Movie)
}
Copy the code

When we implement this new class, we realize that the showDetailView implementation is the same as the old one. We might want to copy and paste this part of the code, but we are such good engineers, how can we allow copy and paste code? OK, let’s take that logic out of the way and call this component Router, again, by accident.

protocol MovieListRouter {
  func showDetailView(for movie: Movie)
}
Copy the code

The Router acts as the spokesperson for the current page and is responsible for displaying the corresponding detail page when needed. But where does this component go? In the old and new versions of the View? That sounds fine but as a rule of thumb, it’s probably best not to change the view code too often unless there’s a real change in requirements. Let’s give the presenter the responsibility to hold the router. When the presenter receives the event, it can decide whether to call the View Model for computation or the Router for navigation. Now that we have reused the navigation logic, we can release it. Let’s take a look at the final code structure:

protocol MovieListView: MovieListPresenterDelegate {
  private var presenter: MovieListPresenter
  func didTapOnReload(a)
  func didTapOnMovie(at index: Int)
}

protocol MovieListPresenterDelegate {
  func updateWithMoviePresentations(_ movies: [MoviePresentation])
}

protocol MovieListPresenter: MovieListViewModelDelegate {
  private var router: MovieListRouter
  private var viewModel: MovieListViewModel
  func reload(a)
  func presentation(from movie: Movie) -> MoviePresentation
}

protocol MovieListRouter {
  func showDetailView(for movie: Movie)
}

protocol MovieListViewModelDelegate: class {
  func viewModelDidUpdate(_ model: MovieListViewModel)
}

protocol MovieListViewModel {
  weak var delegate: MovieListViewModelDelegate? { get set }
  var movies: [Movie] { get }
  func fetchMovies(a)
}
Copy the code

Now, I think you’ve got it, because when we rename the MovieListViewModel to MovieListInteractor, the code becomes 100% VIPER, but it doesn’t violate MVVM rules.

conclusion

Software architecture is simply a bunch of rules. Some have more rules than others. Using one architecture does not mean abandoning the other entirely. Especially when we’re talking about MVC, MVVM and VIPER.

  • Start with a simplified version of VIPER, which is pretty much like MVVM, with views, Interactor and entities.
  • If you want to change the UI quickly, add Presenter.
  • If you have complex and reusable routing logic in your project, add a Router.
  • Before implementing each requirement, design the class diagram and interface. While it is generally accepted that this is not necessary, it can definitely help you design better interfaces and ultimately reduce development time.

Translator’s Summary:

I’ve heard a lot about VIPER before, but I don’t really know much about the details because I didn’t practice it in the project. This article analyzed the difference between VIPER and MVVM from a very good Angle, and I read it very profitably. Therefore, I will translate it into Chinese for myself to review later.

As for architectural patterns, my own view is very similar to the one in this article. I think it doesn’t matter what architectural patterns we choose in the project. Our goal is to decouple and expand easily.

MVC, which has been disused many times in the industry, actually works well in the hands of good programmers, but when it comes to relatively beginner developers, there is the problem of Massive Controller. The main reason for this, I think, is that MVC makes too few rules.

More experienced developers understand the principles of software architecture so that no matter how many or few rules there are in an architectural pattern, the code produced from them is always maintained at an elegant level. Therefore, MVC can have different results in different hands.

MVVM, which has more rules, and VIPER, which has more restrictions on its own rules, keep code quality at a relatively good level regardless of the level of developers following these rules for business development.

Therefore, in my opinion, the choice of architectural pattern depends on the average capability of the team, which in general can be inversely proportional to the number of rules of the architectural pattern.

If you have any questions about the business architecture mode, welcome to discuss together.