📜 ⬆️ ⬇️

Comparing Viper and MVVM architectures: How to apply one and the other



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.

Example

We 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.



MVVM

How it works:

MVVM stands for Model-View-ViewModel . This approach differs from MVC in the logic of the distribution of responsibility between modules.



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 App

MODEL

The 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 .

VIEW

VIEW 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 MODEL

It'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.

VIPER

How 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.



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 App

VIEW

VIEW 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.

INTERACTOR

Interactor 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).

PRESENTER

The 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.

ENTITY

This 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.

ROUTER

And 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.

Source: https://habr.com/ru/post/440904/