
Currently, VIPER and MVVM are the most popular architectural solutions used in the development of large applications that require participation in the development of large teams that are well-tested, long-lasting and constantly evolving. In this article we will try to apply them on a small test project, which is a list of contacts of users with the ability to add a new contact. This article has more practice than analysts, and it is intended primarily for those who are already in theory familiar with these architectures and now would like to understand how this works with specific examples. However, a basic description of the architectures and their comparison is also present.
This article is a translation of the article by Rafael Sacchi
"Comparing MVVM and Viper architectures: When to use one or the other .
" Unfortunately, at some point in the creation of an article, “publication” was put up instead of “translation,” so you have to write here.
A well-designed architecture is very important in order to provide long-term support for your project. In this article, we will look at MVVM and VIPER architectures as an alternative to traditional MVC.
MVC is a well-known concept for all those who have been developing software for quite some time. This pattern divides the project into three parts: Model, representing entities; View, which is an interface for user interaction; and the Controller, responsible for ensuring the interaction between View and Model. This is the architecture that Apple offers us to use in our applications.
However, you probably know that projects come with fairly large and complex functionality: support for network requests, parsing, access to data models, data conversion for output, reaction to interface events, etc. As a result, you get huge controllers that solve the above tasks and a bunch of code that cannot be reused. In other words, MVC can be a nightmare for a developer with long-term project support. But how to ensure high modularity and re-usability in iOS projects?
We will look at two very well-known alternatives to the MVC architecture: MVVM and VIPER. Both of them are quite famous in the iOS community and have proven that they can be an excellent alternative to MVC. We will talk about their structure, write an example application and consider cases when it is better to use a particular architecture.
ExampleWe will write an application with a table of user contacts. You can use the code from
this repository . The Starter folders contain the basic skeleton of the project, and the Final folders contain a complete application.
The application will have two screens: on the first one there will be a list of contacts displayed as a table, the cell will contain the name and surname of the contact, as well as the basic picture instead of the user's image.

The second screen is a screen for adding a new contact, with input fields of the name and surname and the Done and Cancel buttons.
MVVMHow it works:
MVVM stands for
Model-View-ViewModel . This approach differs from MVC in the logic of the distribution of responsibility between modules.
- Model : This module is no different from that in MVC. He is responsible for creating data models and may contain business logic. You can also create helper classes, such as the manager class for managing objects in the Model and the network manager for handling network requests and parsing.
- View : And here everything starts to change. The MVVM View module covers the interface (UIView subclasses, .xib and .storyboard files), display logic (animation, rendering) and user event handling (button presses, etc.) MVC is responsible for View and Controller. This means that the interfaces (views) you will remain unchanged, while the ViewController will contain a small part of what was in it in MVC and, accordingly, greatly reduced.
- ViewModel : This is now the place where most of the code that you previously had in ViewController will be located. The ViewModel layer requests data from the Model (it can be a request to a local database or a network request) and sends them back to View, in the format in which they will be used and displayed there. But this is a bidirectional mechanism, the actions or data entered by the user go through the ViewModel and update the Model. Since the ViewModel keeps track of everything that is displayed, it is useful to use the linking mechanism between the two layers.
Compared to MVC, you are moving from an architecture that looks like this:

To the following architecture:

In which the implementation of the View uses the classes and subclasses of UIView and UIViewController.
Well, now to the point. Let's write an example of our application using the MVVM architecture.
MVVM Contacts AppMODELThe following class is a
Contact contact model:
import CoreData open class Contact: NSManagedObject { @NSManaged var firstName: String? @NSManaged var lastName: String? var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " \(lastName)" } return name } } }
The contact class has the fields
firstName ,
lastName , and the calculated property
fullName .
VIEWVIEW includes: Main storyboard, with a view already placed on it; ContactsViewController, which displays a list of contacts in a table; and AddContactViewController with a couple of labels and input fields to add the name and surname of the new contact. Let's start with
ContactsViewController . Its code will look like this:
import UIKit class ContactsViewController: UIViewController { @IBOutlet var tableView: UITableView! let contactViewModelController = ContactViewModelController() override func viewDidLoad() { super.viewDidLoad() tableView.tableFooterView = UIView() contactViewModelController.retrieveContacts({ [unowned self] in self.tableView.reloadData() }, failure: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let addContactNavigationController = segue.destination as? UINavigationController let addContactVC = addContactNavigationController?.viewControllers[0] as? AddContactViewController addContactVC?.contactsViewModelController = contactViewModelController addContactVC?.didAddContact = { [unowned self] (contactViewModel, index) in let indexPath = IndexPath(row: index, section: 0) self.tableView.beginUpdates() self.tableView.insertRows(at: [indexPath], with: .left) self.tableView.endUpdates() } } } extension ContactsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") as? ContactsTableViewCell guard let contactsCell = cell else { return UITableViewCell() } contactsCell.cellModel = contactViewModelController.viewModel(at: (indexPath as NSIndexPath).row) return contactsCell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactViewModelController.contactsCount } }
Even with a cursory glance it is clear that this class implements mostly interface tasks. It also has navigation in the
prepareForSegue (: :) method - and this is exactly the moment that will change in VIPER when you add the Router layer.
Let's now take a closer look at the class extension that implements the UITableViewDataSource protocol. The functions do not work directly with the contact model of the user Contact in the Model layer — instead they receive data (represented by the ContactViewModel structure) in the form in which they will be displayed, already formatted using the ViewModelController.
The same happens in the closure, which starts immediately after the contact is created. His only task is to add a row to the table and update the interface.
Now it is necessary to establish a connection between the subclass UITableViewCell and ViewModel. So the cell of the cell of the
ContactsTableViewCell table will look like:
import UIKit class ContactsTableViewCell: UITableViewCell { var cellModel: ContactViewModel? { didSet { bindViewModel() } } func bindViewModel() { textLabel?.text = cellModel?.fullName } }
And so the class
AddContactViewController :
import UIKit class AddContactViewController: UIViewController { @IBOutlet var firstNameTextField: UITextField! @IBOutlet var lastNameTextField: UITextField! var contactsViewModelController: ContactViewModelController? var didAddContact: ((ContactViewModel, Int) -> Void)? override func viewDidLoad() { super.viewDidLoad() firstNameTextField.becomeFirstResponder() } @IBAction func didClickOnDoneButton(_ sender: UIBarButtonItem) { guard let firstName = firstNameTextField.text, let lastName = lastNameTextField.text else { return } if firstName.isEmpty || lastName.isEmpty { showEmptyNameAlert() return } dismiss(animated: true) { [unowned self] in self.contactsViewModelController?.createContact(firstName: firstName, lastName: lastName, success: self.didAddContact, failure: nil) } } @IBAction func didClickOnCancelButton(_ sender: UIBarButtonItem) { dismiss(animated: true, completion: nil) } fileprivate func showEmptyNameAlert() { showMessage(title: "Error", message: "A contact must have first and last names") } fileprivate func showMessage(title: String, message: String) { let alertView = UIAlertController(title: title, message: message, preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil)) present(alertView, animated: true, completion: nil) } }
And once again, basically there is work with the UI. Note that AddContactViewController delegates the contact creation functionality in the ViewModelController in the
didClickOnDoneButton (:) function.
VIEW MODELIt's time to talk about a completely new layer for us ViewModel. To begin with, we will create the
ContactViewModel contact
class , which will provide the view we need to display, the <and> functions with parameters will also be defined, to sort the contacts:
public struct ContactViewModel { var fullName: String } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() }
The
ContactViewModelController code will look like this:
class ContactViewModelController { fileprivate var contactViewModelList: [ContactViewModel] = [] fileprivate var dataManager = ContactLocalDataManager() var contactsCount: Int { return contactViewModelList.count } func retrieveContacts(_ success: (() -> Void)?, failure: (() -> Void)?) { do { let contacts = try dataManager.retrieveContactList() contactViewModelList = contacts.map() { ContactViewModel(fullName: $0.fullName) } success?() } catch { failure?() } } func viewModel(at index: Int) -> ContactViewModel { return contactViewModelList[index] } func createContact(firstName: String, lastName: String, success: ((ContactViewModel, Int) -> Void)?, failure: (() -> Void)?) { do { let contact = try dataManager.createContact(firstName: firstName, lastName: lastName) let contactViewModel = ContactViewModel(fullName: contact.fullName) let insertionIndex = contactViewModelList.insertionIndex(of: contactViewModel) { $0 < $1 } contactViewModelList.insert(contactViewModel, at: insertionIndex) success?(contactViewModel, insertionIndex) } catch { failure?() } } }
Note: MVVM does not exactly define how to create a ViewModel. When I want to create a more layered architecture, I prefer to create a ViewModelController that will interact with the Model layer and will be responsible for creating ViewModel objects.
The main thing that is very easy to remember: the ViewModel layer should not be involved in working with the user interface. In order to avoid this, it’s better to just
never import UIKit into a file with ViewModel.
The ContactViewModelController class queries contacts from local storage and tries not to affect the Model layer. It returns data in a format that requires the view to be displayed, and notifies the view when a new contact is added and the data changes.
In real life, this would be a network request, not a request to a local database, but in neither case should none be part of the ViewModel — and work with the network and work with the local database should be provided with the help of their own managers ( managers).
That's all about MVVM. Perhaps this approach will seem to you more testable, supported and distributed than MVC. Now let's talk about VIPER and see how it differs from MVVM.
VIPERHow it works:
VIPER is the implementation of Clean Architecture for iOS projects. Its structure consists of: View, Interactor, Presenter, Entity, and Router. This is really a very distributed and modular architecture that allows you to share responsibility, is very well covered by unit tests and makes your code reusable.
- View : Interface layer, which usually implies UIKit files (including UIViewController). Understandably, in more distributed systems, the UIViewController subclasses must belong to View. In VIPER, things are almost the same as in MVVM: View is responsible for displaying what Presenter gives and for transferring user-entered information or actions to Presenter.
- Interactor : Contains the business logic necessary for the operation of the application. Interactor is responsible for receiving data from the Model (network or local requests) and its implementation is in no way associated with the user interface. It is important to remember that network and local managers are not part of VIPER, but are treated as separate dependencies.
- Presenter : Responsible for formatting data for display in View. In MVVM in our example, ViewModelController was responsible for this. Presenter obtains data from Interactor, creates an instance of ViewModel (formatted class for correct display) and sends it to View. He also responds to user input, requests additional data from the database, or vice versa, sends them to her.
- Entity : Take responsibility for the Model layer, which is used in other architectures. Entity is simple data objects, without business logic, controlled by an intractor and various data managers.
- Router : All application navigation logic. It may seem that this is not the most important layer, but if you need, for example, to reuse the same view on the iPhone and on the iPad application, the only thing you can change is how your view will appear on the screen. This allows you not to touch any other layers except Router, who will be responsible for this in each case.
Compared to MVVM, VIPER has several key differences in the distribution of responsibility:
- it has a Router, a separate layer responsible for navigation
- Entities are simple data objects, thereby redistributing responsibility for accessing data from Model to Interactor
- duties of ViewModelController are distributed between Interactor and Presenter
And now let's repeat the same application, but on VIPER. But for ease of understanding, we will only make a controller with contacts. The code for the controller for adding a new contact can be found in the project by reference (VIPER Contacts Starter folder in
this repository ).
Note : If you decide to do your project on VIPER, then you should not try to create all the files manually - you can use one of the code generators, for example, such as
VIPER Gen or
Generamba (Rambler project) .
VIPER Contacts AppVIEWVIEW is represented by elements from the Main.storyboard and ContactListView class. VIEW is very passive; Its only tasks are to pass interface events to the Presenter and update its state, upon notification from the Presenter. This is what the
ContactListView code looks like:
import UIKit class ContactListView: UIViewController { @IBOutlet var tableView: UITableView! var presenter: ContactListPresenterProtocol? var contactList: [ContactViewModel] = [] override func viewDidLoad() { super.viewDidLoad() presenter?.viewDidLoad() tableView.tableFooterView = UIView() } @IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) { presenter?.addNewContact(from: self) } } extension ContactListView: ContactListViewProtocol { func reloadInterface(with contacts: [ContactViewModel]) { contactList = contacts tableView.reloadData() } func didInsertContact(_ contact: ContactViewModel) { let insertionIndex = contactList.insertionIndex(of: contact) { $0 < $1 } contactList.insert(contact, at: insertionIndex) let indexPath = IndexPath(row: insertionIndex, section: 0) tableView.beginUpdates() tableView.insertRows(at: [indexPath], with: .right) tableView.endUpdates() } } extension ContactListView: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") else { return UITableViewCell() } cell.textLabel?.text = contactList[(indexPath as NSIndexPath).row].fullName return cell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactList.count } }
View sends to the Presenter the events
viewDidLoad and
didClickOnAddButton . On the first event, the Presenter will request data from Interactor, and on the second, the Presenter will request Router to switch to the controller for adding a new contact.
The protocol methods ContactListViewProtocol are called from Presenter, either when a contact list request is made, or when a new contact is added. In this and in another case, the data in the View contain only the information that is necessary for display.
Also in View there are methods that implement the UITableViewDataSource protocol, which fill the table with the received data.
INTERACTORInteractor is pretty simple in our example. All he does is request data through a local database manager, and it doesn’t matter to him that this manager, CoreData, Realm, or some other solution is using it. The code in the ContactListInteractor is as follows:
class ContactListInteractor: ContactListInteractorInputProtocol { weak var presenter: ContactListInteractorOutputProtocol? var localDatamanager: ContactListLocalDataManagerInputProtocol? func retrieveContacts() { do { if let contactList = try localDatamanager?.retrieveContactList() { presenter?.didRetrieveContacts(contactList) } else { presenter?.didRetrieveContacts([]) } } catch { presenter?.didRetrieveContacts([]) } } }
After Interactor receives the requested data, it notifies the Presenter. Still, as an option, Interactor can pass an error to Presenter, which will then have to format the error into a form suitable for display in View.
Note : As you can see, each layer in VIPER implements a protocol. As a result, classes depend on abstractions, and not on a specific implementation, thus complying with the principle of dependency inversion (one of the SOLID principles).
PRESENTERThe most important element of architecture. All communication between the View and the remaining layers (Interactor and Router) passes through the Presenter.
ContactListPresenter Code:
class ContactListPresenter: ContactListPresenterProtocol { weak var view: ContactListViewProtocol? var interactor: ContactListInteractorInputProtocol? var wireFrame: ContactListWireFrameProtocol? func viewDidLoad() { interactor?.retrieveContacts() } func addNewContact(from view: ContactListViewProtocol) { wireFrame?.presentAddContactScreen(from: view) } } extension ContactListPresenter: ContactListInteractorOutputProtocol { func didRetrieveContacts(_ contacts: [Contact]) { view?.reloadInterface(with: contacts.map() { return ContactViewModel(fullName: $0.fullName) }) } } extension ContactListPresenter: AddModuleDelegate { func didAddContact(_ contact: Contact) { let contactViewModel = ContactViewModel(fullName: contact.fullName) view?.didInsertContact(contactViewModel) } func didCancelAddContact() {} }
After the View is loaded, it notifies Presenter, which in turn requests data through Interactor. When a user clicks the add new contact button, View notifies the Presenter, which sends a request to open the add new contact screen to the Router.
The Presenter also formats the data and returns it to the View after the contact list request is made. It is also responsible for implementing the AddModuleDelegate protocol. This means that the Presenter will receive a notification when a new contact is added, prepare the contact data for display and transmit to View.
As you can see, the Presenter has every chance of becoming quite cumbersome. If there is such a probability, the Presenter can be divided into two parts: The Presenter, which only receives data, formats it for display and sends to View; and an event handler that will respond to user actions.
ENTITYThis layer is similar to the Model layer in MVVM. In our application, it is represented by the Contact class and the definition functions for the <and> operators.
Contact content will look like this:
import CoreData open class Contact: NSManagedObject { var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " " + lastName } return name } } } public struct ContactViewModel { var fullName = "" } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() }
ContactViewModel contains fields that Presenter fills (formats) and which View displays. The Contact class is a subclass of NSManagedObject that contains the same fields as in the CoreData model.
ROUTERAnd finally, last but not least, a layer. All responsibility for navigation lies with the Presenter and WireFrame. Presenter receives an event from the user and knows when to make the transition, and WireFrame knows how and where to make this transition. So that you are not confused, in this example the Router layer is represented by the class ContactListWireFrame and is referred to in the text as WireFrame.
ContactListWireFrame code:
import UIKit class ContactListWireFrame: ContactListWireFrameProtocol { class func createContactListModule() -> UIViewController { let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController") if let view = navController.childViewControllers.first as? ContactListView { let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter() let interactor: ContactListInteractorInputProtocol = ContactListInteractor() let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager() let wireFrame: ContactListWireFrameProtocol = ContactListWireFrame() view.presenter = presenter presenter.view = view presenter.wireFrame = wireFrame presenter.interactor = interactor interactor.presenter = presenter interactor.localDatamanager = localDataManager return navController } return UIViewController() } static var mainStoryboard: UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) } func presentAddContactScreen(from view: ContactListViewProtocol) { guard let delegate = view.presenter as? AddModuleDelegate else { return } let addContactsView = AddContactWireFrame.createAddContactModule(with: delegate) if let sourceView = view as? UIViewController { sourceView.present(addContactsView, animated: true, completion: nil) } } }
Since WireFrame is responsible for creating the module, it will be convenient to configure all dependencies here. When you want to open another controller, the function that opens a new controller receives as an argument the object that will open it, and creates a new controller using its WireFrame. Also, when creating a new controller, the necessary data is transferred to it, in this case only the delegate (the Presenter of the controller with contacts) to receive the created contact.
The Router layer gives you a good opportunity to avoid using segues (transitions) in storyboards and organize all navigation code. Since storyboards do not provide a compact solution for transferring data between controllers, our navigation implementation will not add extra code. All we get is just the best advantage.
Results :
You can find both projects in
this repository .
As you can see, MVVM and VIPER, though different, are not unique. MVVM tells us that besides View and Model there must be another ViewModel layer. But nothing is said about how this layer should be created, nor about how the data is requested - the responsibility for this layer is not clearly defined. There are many ways to implement it and you can use any of them.
On the other hand, VIPER is a rather unique architecture. It consists of many layers, each of which has a well-defined area of responsibility and is less than MVVM influenced by the developer.
When it comes to the choice of architecture, the only right decision is usually not, but still I will try to give a couple of tips. If you have a large and long-term project, with clear requirements and you want to have ample opportunities to reuse components, then VIPER will be the best solution. A clearer delineation of responsibility makes it possible to better organize testing and improve reusability.