📜 ⬆️ ⬇️

Architectural Patterns in iOS

Introduction to MVP, MVC, MVVM and VIPER. What is common between them and what is the difference.



Do everything in MVC, and it turns out ugly? Doubt whether to switch to MVVM? Have you heard about VIPER, but are not sure whether it is worth it?

In this article, I will briefly review some of the popular architectural patterns in iOS and compare them in theory and in practice. You will find more information when clicking on the links indicated in the text.

Mastering patterns can be addictive, so be careful: in
In the end, you may be asking yourself more questions than before reading this article, for example:
- Who should own network requests: Model or Controller?
- How can I transfer the Model to the ViewModel of the new View?
- Who creates the new VIPER module: Router or Presenter?



Why it is worth taking care of the choice of architecture?


Because if you do not do this, then one day, debugging a huge class with dozens of different methods and properties, you will not be able to find and correct mistakes in it. Naturally, such a class is difficult to keep in mind as a whole, so you will always lose sight of any important details. If you are already in this situation, then it is very likely that:

And this can happen even if you follow Apple's recommendations and implement their Cocoa MVC pattern , so don't be upset. “Apple” MVC is not all right, but we will return to it later.
')
And now let's define the signs of a good architecture:

Why distribution?


Distribution reduces the load on the brain when we try to figure out how this or that entity works. If you think that the more you develop, the better the brain will adapt to understanding complex concepts - you are right. But everything has a limit, and it is reached rather quickly. Thus, the easiest way to reduce complexity is to divide responsibilities between multiple entities on the basis of shared responsibility .

Why testability?


The testability of the architecture determines how easy it will be for us to write unit tests, and more often if we can write them in principle. Is it worth testing at all? As a rule, it is not a question for those who have failed unit tests after adding a new functionality or after refactoring some class subtleties. This means that the tests have saved the developers from detecting problems in the runtime. What could have happened to the application already on the users device, and the correction would be possible only in a week .

Why ease of use?


Everything is clear, but it is worth noting that the best code is the code that has never been written. And the less code you have, the fewer errors. Therefore, the desire to write less code does not mean that the developer is lazy. And choosing the smartest solution, you should always consider the cost of its support.

The basics of MV (X)


Today we have many options for architectural design patterns:

The first three of them involve assigning application entities to one of 3 categories:

Having shared entities, we can:

Let's start with the MV (X) patterns and return to VIPER later.

MVC


As it was before


Before discussing Apple’s MVC vision, let's look at the traditional version .


In the traditional MVC View does not store state in itself. The controller simply renders the View when the Model changes. For example, a web page is completely reloaded after you click on a link to go to another location. Although it is possible to implement traditional MVC in the iOS environment, this does not make much sense due to the architectural problem: all three entities are closely related, each entity knows about the other two. This greatly reduces the ability to reuse each of the elements. For this reason, we will not even try to write an example of canonical MVC.

Traditional MVC seems inapplicable to modern iOS development.

Apple's MVC


Expectations




Controller is an intermediary between View and Model , therefore, the last two are not aware of the existence of each other. Therefore, the Controller is difficult to reuse, but this, in principle, suits us, since we must have a place for that tricky business logic that does not fit into the Model .

In theory, everything looks very simple, but you feel that something is wrong, right? You have probably heard that people decipher MVC as Massive View Controller . In addition, ViewController unloading has become an important topic for iOS developers. Why does this happen if Apple just took the traditional MVC and improved it a little?

Reality




Cocoa MVC encourages you to write Massive View Controller, because the controller is so involved in the View life cycle that it is difficult to say that it is a separate entity. Although you still have the opportunity to ship some of the business logic and data transformation in the Model , when it comes to shipping work in View , you have few options. In most cases, the entire responsibility of the View is to send actions to the controller. As a result, everything ends with the View Controller becoming a delegate and data source, as well as a place to start and cancel server requests and, in general, everything.

How many times have you seen this code:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell userCell.configureWithUser(user) 

View- cell is configured directly from the Model . Thus, the principles of MVC are violated, but such code can be seen very often, and, as a rule, people do not understand that this is wrong. If you strictly follow MVC, you must configure the cell inside the controller and not transfer the Model to the View, which will increase the Controller even more.

Cocoa MVC is reasonably decoded as Massive View Controller.

The problem is not obvious until it comes to unit tests (I hope that in your project it still comes). Since the View Controller is closely connected with the View , it becomes difficult to test, and you have to go in a sophisticated way, replacing the View with Mock objects and simulating their life cycle, as well as writing the View Controller code so that the business logic is maximally separated from the code view layout.

Let's look at a simple example from the playground:
 import UIKit struct Person { // Model let firstName: String let lastName: String } class GreetingViewController : UIViewController { // View + Controller var person: Person! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName self.greetingLabel.text = greeting } // layout code goes here } // Assembling of MVC let model = Person(firstName: "David", lastName: "Blaine") let view = GreetingViewController() view.person = model; 

The MVC build can be performed in a “presenting” View Controller.

It seems to be hard to test, right? We can allocate the generation of greetings to the new GreetingModel class and test it separately, but we cannot test the presentation logic (even though there is not much of it in the example) inside the GreetingViewController without calling the View life cycle methods directly ( viewDidLoad, didTapButton ), which can lead to loading all UIView, and this is bad for unit tests.

In fact, testing UIViews on one simulator (for example, iPhone 4S) does not guarantee that it will work properly on other devices (for example, iPad), so I recommend removing the Host Application tick from the unit test configuration and running it on simulator, not including the application itself.

The interaction between View and Controller is not really amenable to testing with unit tests .

After all this, it may seem that Cocoa MVC is a rather bad choice of pattern. But let's evaluate it in terms of features of a good architecture , defined at the beginning of the article:

Cocoa MVC is a smart choice if you are not willing to invest a lot of time in your architecture and feel that the higher service cost pattern is not affordable for your small project or startup.

Cocoa MVC is the best architectural pattern in terms of speed of development.

MVP


Implementing Cocoa MVC Promises




Doesn't it look like an apple MVC? In fact - very, and his name - MVP (option with a passive View ). But does this mean that Apple's MVC is in fact MVP? No, it is not, because, as you remember, View and Controller are closely related there, while the MVP intermediary Presenter is not related to the View Controller life cycle. The View can be easily replaced by Mock objects , so Presenter has no layout code, but it is responsible for updating the View with new data and state.


- What if I tell you that the UIViewController is a View .

From the MVP point of view, the UIViewController subclasses are actually View , not Presenter . This distinction provides excellent testability, which comes at the expense of development speed, because you have to manually link data and events between View and Presenter , as can be seen in the example below.

 import UIKit struct Person { // Model let firstName: String let lastName: String } protocol GreetingView: class { func setGreeting(greeting: String) } protocol GreetingViewPresenter { init(view: GreetingView, person: Person) func showGreeting() } class GreetingPresenter : GreetingViewPresenter { unowned let view: GreetingView let person: Person required init(view: GreetingView, person: Person) { self.view = view self.person = person } func showGreeting() { let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName self.view.setGreeting(greeting) } } class GreetingViewController : UIViewController, GreetingView { var presenter: GreetingViewPresenter! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { self.presenter.showGreeting() } func setGreeting(greeting: String) { self.greetingLabel.text = greeting } // layout code goes here } // Assembling of MVP let model = Person(firstName: "David", lastName: "Blaine") let view = GreetingViewController() let presenter = GreetingPresenter(view: view, person: model) view.presenter = presenter 

Important note regarding assembly


MVP is the first pattern to reveal an assembly problem that occurs due to the presence of three really separate layers. Since we do not need View to know about the Model , it is wrong to build in the presenting View Controller (which is actually View ), therefore, it needs to be done elsewhere. For example, you can create a Router service that will be responsible for building and presenting a View-to-View . This problem occurs not only in MVP, it also needs to be addressed in all subsequent patterns .

Let's look at the signs of a good architecture for MVP:

MVP in iOS means excellent testability and a lot of code.

MVP


With Blackjack and Binding


There is another option MVP - MVP with a supervisory controller. It includes the direct binding of View and Model , while the Presenter (supervisory controller) still handles the actions of the View and is able to modify it.


But, as we learned earlier, the vague division of responsibility is bad in itself, as well as the close connection between View and Model . And I see no point in writing an example for bad architecture.

MVVM


The newest of the MV (X) species.


MVVM is the newest of the MV (X) patterns, so let's hope that it appeared with all the problems inherent in MV (X).

In theory, the Model-View-ViewModel looks very good. View and Model are already familiar to us, as is the View Model as an intermediary.


It is very similar to MVP:

In addition, he does the binding as a supervising version of MVP, but not between View and Model , but between View and View Model .

So what is a View Model in iOS? This is a UIKit independent View view and its state. View Model causes changes in the Model and independently updated with the already updated Model . And since the binding occurs between the View and the View Model , the first one, respectively, is also updated.

Binding


I mention them, starting with the MVP part, but let's take a closer look at them. Bindings are available out of the box for developing OS X, but they are not in the arsenal of an iOS developer. Of course, we have KVO and Notifications, but they are not as comfortable as binding.

Therefore, provided that we do not want to write them ourselves, you can choose:

Today, in fact, when you hear MVVM, you think about ReactiveCocoa, and vice versa. Although you can do MVVM with simple bindings, ReactiveCocoa (or his classmates) will allow you to squeeze everything from the MVVM pattern.

There is one bitter truth about FRP frameworks: great power comes with great responsibility. It is very easy to break everything when you write reactively . In other words, if something went wrong, you can spend a lot of time debugging the application. Just take a look at this call stack.


In our simple example, a reactive framework or even KVO is redundant. We explicitly ask the View Model to update it using the showGreeting method, and use a simple property for the callback greetingDidChange function to learn about the changes.

 import UIKit struct Person { // Model let firstName: String let lastName: String } protocol GreetingViewModelProtocol: class { var greeting: String? { get } var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change init(person: Person) func showGreeting() } class GreetingViewModel : GreetingViewModelProtocol { let person: Person var greeting: String? { didSet { self.greetingDidChange?(self) } } var greetingDidChange: ((GreetingViewModelProtocol) -> ())? required init(person: Person) { self.person = person } func showGreeting() { self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName } } class GreetingViewController : UIViewController { var viewModel: GreetingViewModelProtocol! { didSet { self.viewModel.greetingDidChange = { [unowned self] viewModel in self.greetingLabel.text = viewModel.greeting } } } let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside) } // layout code goes here } // Assembling of MVVM let model = Person(firstName: "David", lastName: "Blaine") let viewModel = GreetingViewModel(person: model) let view = GreetingViewController() view.viewModel = viewModel 

And back to our assessment of signs of good architecture :

MVVM is a very attractive pattern, as it combines the advantages of the aforementioned approaches and does not require additional code for updating the View in connection with the side view bindings. However, testability is still at a good level.

VIPER


Building experience from Lego cubes, transferred to the design of iOS applications


VIPER is our last candidate, which is especially interesting because it is not from the category MV (X).

By now you should already agree that the division of responsibilities is very good. VIPER takes another step towards the separation of duties and instead of the usual three layers offers five .


In principle, the VIPER module can be a single screen or a whole user story of your application (for example, authentication can be on one screen or several related screens). It’s up to you how small your “lego-blocks” will be.

If we compare VIPER with MV (X) -type patterns, we will see several differences in the distribution of responsibilities:

The fact that MV (X) -patterns do not solve the routing problem does not mean that it does not exist for iOS applications.

In the example, there is no routing or interaction between modules, since these topics are not covered at all by the MV (X) -parts.
 import UIKit struct Person { // Entity (usually more complex eg NSManagedObject) let firstName: String let lastName: String } struct GreetingData { // Transport data structure (not Entity) let greeting: String let subject: String } protocol GreetingProvider { func provideGreetingData() } protocol GreetingOutput: class { func receiveGreetingData(greetingData: GreetingData) } class GreetingInteractor : GreetingProvider { weak var output: GreetingOutput! func provideGreetingData() { let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layer let subject = person.firstName + " " + person.lastName let greeting = GreetingData(greeting: "Hello", subject: subject) self.output.receiveGreetingData(greeting) } } protocol GreetingViewEventHandler { func didTapShowGreetingButton() } protocol GreetingView: class { func setGreeting(greeting: String) } class GreetingPresenter : GreetingOutput, GreetingViewEventHandler { weak var view: GreetingView! var greetingProvider: GreetingProvider! func didTapShowGreetingButton() { self.greetingProvider.provideGreetingData() } func receiveGreetingData(greetingData: GreetingData) { let greeting = greetingData.greeting + " " + greetingData.subject self.view.setGreeting(greeting) } } class GreetingViewController : UIViewController, GreetingView { var eventHandler: GreetingViewEventHandler! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { self.eventHandler.didTapShowGreetingButton() } func setGreeting(greeting: String) { self.greetingLabel.text = greeting } // layout code goes here } // Assembling of VIPER module, without Router let view = GreetingViewController() let presenter = GreetingPresenter() let interactor = GreetingInteractor() view.eventHandler = presenter presenter.view = view presenter.greetingProvider = interactor interactor.output = presenter 

And yet, once again return to the signs .

So what about Lego?


When using VIPER, it may seem to you that you are building the Empire State Building from Lego cubes, and this suggests that you have problems . Maybe you took up VIPER too early and it is worth looking at something simpler. Some people ignore it and continue to shoot from a cannon on sparrows. I assume that they believe that their applications will benefit from VIPER sometime in the future, even if the cost of service is now unreasonably high. If you think it's worth it, then I recommend you try Generamba , a tool for generating VIPER skeletons. Although personally it seems to me that this is akin to using an automatic sight for shooting from the same gun instead of a slingshot .

Conclusion


We looked at several architectural patterns, and I hope that you have found answers to some of your questions. I have no doubt that you understood that there is no “silver bullet” among the patterns, and the choice of architecture is a matter of weighing compromises in your particular situation.
It seems to me quite natural to combine several architectures in one application. For example, you started with MVC, but having understood that a particular screen (use case) became too difficult to maintain with MVC, you switched to MVVM, but only for that particular screen. Because in fact there is no need to refactor other screens for which MVC work perfectly, especially since both architectures are easily compatible.

Make it as simple as possible, but not simpler. (c) Albert Einstein


An English version is available here . The slides that I presented at NSLondon are available here .

Bogdan Orlov,
iOS developer in Badoo

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


All Articles