├── Resources ├── cover.jpg ├── MVCMassacre │ ├── MVC.png │ ├── FirstExampleResult.png │ ├── Storyboard editor.png │ ├── ThirdExampleResult.png │ ├── SecondExampleResult.png │ ├── MassiveViewControllerExample.png │ └── MassiveViewControllerScheme.png ├── webview-scheme.png ├── clean-architecture.png ├── module_structure.png ├── submodules │ ├── submodules.001.png │ ├── submodules.002.png │ ├── submodules.003.png │ ├── submodules.004.png │ ├── submodules.005.png │ ├── submodules.006.png │ ├── submodules.007.png │ ├── submodules.008.png │ ├── submodules.009.png │ ├── submodules.010.png │ └── submodules.011.png └── instagram_example_serkrapiv.png ├── english ├── contents.md ├── introduction-to-viper.md ├── code-generation.md ├── other-materials.md ├── module-structure.md ├── webview.md └── mvc-chainsaw-massacre.md ├── russian ├── contents.md ├── introduction-to-viper.md ├── rambler-materials.md ├── code-generation.md ├── module-transitions.md ├── testing.md ├── other-materials.md ├── module-structure.md ├── compound-modules.md ├── webview.md ├── frc.md ├── code-style.md └── mvc-chainsaw-massacre.md ├── README.md └── LICENSE /Resources/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/cover.jpg -------------------------------------------------------------------------------- /Resources/MVCMassacre/MVC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/MVCMassacre/MVC.png -------------------------------------------------------------------------------- /Resources/webview-scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/webview-scheme.png -------------------------------------------------------------------------------- /Resources/clean-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/clean-architecture.png -------------------------------------------------------------------------------- /Resources/module_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/module_structure.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.001.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.002.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.003.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.004.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.005.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.006.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.007.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.008.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.009.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.010.png -------------------------------------------------------------------------------- /Resources/submodules/submodules.011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/submodules/submodules.011.png -------------------------------------------------------------------------------- /Resources/instagram_example_serkrapiv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/instagram_example_serkrapiv.png -------------------------------------------------------------------------------- /Resources/MVCMassacre/FirstExampleResult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/MVCMassacre/FirstExampleResult.png -------------------------------------------------------------------------------- /Resources/MVCMassacre/Storyboard editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/MVCMassacre/Storyboard editor.png -------------------------------------------------------------------------------- /Resources/MVCMassacre/ThirdExampleResult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/MVCMassacre/ThirdExampleResult.png -------------------------------------------------------------------------------- /Resources/MVCMassacre/SecondExampleResult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/MVCMassacre/SecondExampleResult.png -------------------------------------------------------------------------------- /Resources/MVCMassacre/MassiveViewControllerExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/MVCMassacre/MassiveViewControllerExample.png -------------------------------------------------------------------------------- /Resources/MVCMassacre/MassiveViewControllerScheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongself/The-Book-of-VIPER/HEAD/Resources/MVCMassacre/MassiveViewControllerScheme.png -------------------------------------------------------------------------------- /english/contents.md: -------------------------------------------------------------------------------- 1 | ![VIPER](http://i.imgur.com/z0BTfgi.png) 2 | 3 | ### Contents 4 | 5 | #### Basics 6 | - [Introduction to VIPER](introduction-to-viper.md) 7 | - [Structure of the VIPER module](module-structure.md) 8 | - [Code generation and module creation automation](code-generation.md) 9 | 10 | #### Practice 11 | - Compound VIPER modules 12 | - Transitions between modules 13 | - [The road from MVC to VIPER](mvc-chainsaw-massacre.md) 14 | - VIPER and NSFetchedResultsController 15 | - [UIWebView and VIPER](webview.md) 16 | - VIPER and TDD 17 | 18 | #### Other 19 | - Code Style Questions 20 | - Additional Materials by Rambler&Co 21 | - [Other Materials](other-materials.md) 22 | -------------------------------------------------------------------------------- /russian/contents.md: -------------------------------------------------------------------------------- 1 | ![VIPER](http://i.imgur.com/z0BTfgi.png) 2 | 3 | ### Содержание 4 | 5 | #### Основы 6 | - [Введение в VIPER](introduction-to-viper.md) 7 | - [Структура VIPER-модуля](module-structure.md) 8 | - [Вопросы кодогенерации и автоматизации создания модулей](code-generation.md) 9 | 10 | #### Практика 11 | - [Составные VIPER модули](compound-modules.md) 12 | - [Переходы между модулями](module-transitions.md) 13 | - [Путь от Massive ViewController до VIPER](mvc-chainsaw-massacre.md) 14 | - [VIPER и NSFetchedResultsController](frc.md) 15 | - [Работа с UIWebView в VIPER](webview.md) 16 | - [VIPER и TDD](testing.md) 17 | 18 | #### Прочее 19 | - [Вопросы Code Style](code-style.md) 20 | - [Дополнительные материалы Rambler&Co](rambler-materials.md) 21 | - [Подборка сторонних материалов по VIPER](other-materials.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Book of VIPER 2 | 3 | ![](https://img.shields.io/badge/license-CC--BY--NC--SA%204.0%20Int-blue.svg) ![](https://img.shields.io/badge/russian-100%25-brightgreen.svg) ![](https://img.shields.io/badge/english-20%25-red.svg) 4 | 5 | This book is the most complete guide to the **VIPER architecture**. It covers almost everything - from module structure and history of VIPER to code style questions and complex practical examples of using this pattern in the wild. 6 | 7 | ![Cover](Resources/cover.jpg) 8 | 9 | ## Downloads 10 | 11 | The russian version of the book is available in multiple formats: 12 | 13 | - [PDF](https://www.gitbook.com/download/pdf/book/etolstoy/the-book-of-viper) 14 | - [ePub](https://www.gitbook.com/download/epub/book/etolstoy/the-book-of-viper) 15 | - [Mobi](https://www.gitbook.com/download/mobi/book/etolstoy/the-book-of-viper) 16 | - [Read Online](https://www.gitbook.com/read/book/etolstoy/the-book-of-viper) 17 | 18 | ## Translation 19 | 20 | We aim to translate the Book to as many languages as possible. You can help us with it! Join us: 21 | 22 | - [Russian content](russian/contents.md) - 100% complete 23 | - [English content](english/contents.md) - [14% complete](https://github.com/strongself/The-Book-of-VIPER/issues?q=is%3Aissue+is%3Aopen+label%3Aenglish) 24 | 25 | ## Authors 26 | 27 | [Rambler.iOS Team and outside collaborators](https://github.com/strongself/The-Book-of-VIPER/graphs/contributors) 28 | 29 | ## License 30 | 31 | [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://github.com/strongself/The-Book-of-VIPER/blob/master/LICENSE) 32 | -------------------------------------------------------------------------------- /english/introduction-to-viper.md: -------------------------------------------------------------------------------- 1 | **VIPER** - Architecture approach for application development(particularly iOS), based on [the Clean Architecture](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) by [Robert C. Martin (Uncle Bob)](http://blog.cleancoder.com/). 2 | 3 | ![Clean Architecture](../Resources/clean-architecture.png) 4 | 5 | **Main goals of VIPER**: 6 | 7 | - Increasing test coverage of the Presentation level, usually built from *Massive View Controllers*. 8 | - Conforming to the Single Responsibility Principle. 9 | 10 | It is important to note that VIPER is not the list of rules and templates. It's the list of recommendations how to build flexible, testable and reusable architecture. We, the iOS Team of **Rambler&Co**, have adopted some canonical principles and formed our best practices for some cases. 11 | 12 | VIPER looks difficult at first, especially for developers without the team work experiences on large projects. It might be hard to understand its benefits if independent modules and high test coverage are not your priorities. But, VIPER is helpful even for small applications. 13 | 14 | **Pros and cons of VIPER:** 15 | 16 | Pros: 17 | 18 | - **Increase of testability** for the application presentation layer. 19 | - **Modules are independent** from each other. It separates the development environment and increases the code reusability. 20 | - **Main architecture approaches are defined**. So it's much easier to add new developer to the team or move project to another team. 21 | 22 | Cons: 23 | 24 | - Highly **increases number of classes** in the project, as well as the complexity of creating a new module. 25 | - Some principles **don't work with UIKit** out of the box. 26 | - **Lack of recommendations**, best practices and complex application examples. 27 | 28 | We'll cover each of these concepts in detail and focus on how to solve those problems through the book. 29 | 30 | **VIPER history timeline:** 31 | 32 | - **08.2012** - An article [The Clean Architecture](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) by Robert Martin 33 | - **12.2013** - An article [Introduction to VIPER](http://mutualmobile.github.io/blog/2013/12/04/viper-introduction/) by [MutualMobile](http://mutualmobile.github.io/) 34 | - **06.2014** - Issue #13. objc.io [Architecting iOS Apps with VIPER](https://www.objc.io/issues/13-architecture/viper/) by MutualMobile 35 | - **07.2014** - [iPhreaks Show podcast](https://itunes.apple.com/ru/podcast/the-iphreaks-show/id634022060?mt=2&i=316803444) by MutualMobile. History of VIPER, goals and usage. 36 | - **04.2015** - Rambler&Co VIPER hackatone leads to first application. 37 | - **12.2015** - Rambler&Co has dozen of VIPER application in development and [released in AppStore](https://itunes.apple.com/ru/developer/rambler-internet-holdings/id395455934). 38 | -------------------------------------------------------------------------------- /russian/introduction-to-viper.md: -------------------------------------------------------------------------------- 1 | **VIPER** - это подход к архитектуре мобильных приложений (в частности - iOS), основанный на идеях [Роберта Мартина](http://blog.cleancoder.com/), изложенных им в статье [The Clean Architecture](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html). 2 | 3 | ![Clean Architecture](../Resources/clean-architecture.png) 4 | 5 | #### Основные задачи, которые помогает решить VIPER 6 | 7 | - Обеспечение более полного покрытия тестами слоя Presentation, обычно включающего в себя *Massive View Controllers*. 8 | - Разбитие самых крупных классов наших приложений на набор элементов с более-менее четко определенными границами ответственности. 9 | 10 | Важно сразу же отметить, что VIPER - это ни в коем случае не набор строгих шаблонов и правил. Скорее это перечень рекомендаций, следуя которым можно построить гибкую и переиспользуемую архитектуру мобильного приложения. Мы, iOS команда Rambler&Co, адаптировали некоторые из каноничных принципов и сформировали определенный набор Best Practices для разработки тех или иных юзкейсов. 11 | 12 | Первоначально VIPER может ломать сознание, особенно разработчикам без опыта командной работы над крупными проектами - отсутствует понимание необходимости независимости модулей приложения друг от друга и максимально возможного покрытия их тестами. Тем не менее, весь набор решений оправдывает себя даже для небольших приложений. 13 | 14 | #### Основные достоинства и недостатки VIPER 15 | 16 | Плюсы: 17 | 18 | - Повышение тестируемости Presentation-слоя приложений. 19 | - Полная независимость модулей друг от друга - это позволяет независимо их разрабатывать и переиспользовать как в одном приложении, так и в нескольких. 20 | - Передача проекта другим разработчикам, либо внедрение нового, дается намного проще, так как общие подходы к архитектуре заренее определены. 21 | 22 | Минусы: 23 | 24 | - Резкое увеличение количества классов в проекте, сложности при создании нового модуля. 25 | - Некоторые из принципов не ложатся напрямую на UIKit и подходы Apple. 26 | - Отсутствие в открытом доступе набора конкретных рекомендаций, best practices и примеров сложных приложений. 27 | 28 | Остальные части нашего руководства в подробностях раскроют каждый из этих пунктов, в том числе расскажут о том, как избавиться от перечисленных недостатков. 29 | 30 | #### Небольшой ликбез по истории вопроса 31 | 32 | - **08.2012** - Статья [The Clean Architecture](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) от Роберта Мартина. 33 | - **12.2013** - Статья [Introduction to VIPER](http://mutualmobile.github.io/blog/2013/12/04/viper-introduction/) от компании [MutualMobile](http://mutualmobile.github.io/). 34 | - **06.2014** - Выпуск objc.io #13 со статьей [Architecting iOS Apps with VIPER](https://www.objc.io/issues/13-architecture/viper/) от тех же MutualMobile. 35 | - **07.2014** - [Выпуск подкаста iPhreaks Show](https://itunes.apple.com/ru/podcast/the-iphreaks-show/id634022060?mt=2&i=316803444), в котором MutualMobile рассказывают о том, как появился VIPER, какие вопросы он решает, и как используется в их приложениях. 36 | - **04.2015** - В рамках локального хакатона в Rambler&Co пишется первое приложение с использованием подходов VIPER. 37 | - **12.2015** - У Rambler&Co больше десяти приложений на VIPER, как разрабатываемых в данный момент, так и [выпущенных в AppStore](https://itunes.apple.com/ru/developer/rambler-internet-holdings/id395455934). -------------------------------------------------------------------------------- /english/code-generation.md: -------------------------------------------------------------------------------- 1 | Creating a new VIPER module can be rather cumbersome and involves at least providing the following: 2 | 3 | - **Five new classes** (Assembly, ViewController, Presenter, Interactor, Router), 4 | - **Five new protocols** (ViewInput, ViewOutput, InteractorInput, InteractorOutput, RouterInput), 5 | - **Five new tests** (AssemblyTests, ViewControllerTests, PresenterTests, InteractorTests, RouterTests). 6 | 7 | Moreover, all the necessary relations need to be established, protocols need to be implemented, dependency injection set up - and plenty of other things in particular cases. This complexity brings two main problems: 8 | 9 | - Too much time spent on routine work 10 | - Typos become more frequent which adversely affects the code style and application logic 11 | 12 | One way to tackle the issue is via creating Xcode templates (we were also following this approach some time ago in Rambler&Co). It solves the problems mentioned above, though having a number of disadvantages: 13 | 14 | * Building a new template is a complex process due to cumbersome syntax 15 | * Templates might just stop working after an Xcode update 16 | * No convenient way of adding new templates to Xcode (Alcatraz does not count) 17 | * Impossible to add template files to different targets (e.g. when generating auto tests) 18 | * Xcode templates are highly developer dependent, not the project dependent 19 | 20 | To be independent of Xcode IDE and its implementation details, we have decided to separate the process of code generation and came up with a small utility application - [Generamba](https://github.com/rambler-digital-solutions/Generamba). 21 | 22 | ![Generamba](http://s24.postimg.org/gej9cg1cl/generamba.jpg) 23 | 24 | --- 25 | 26 | ### Installation 27 | 28 | ``` 29 | gem install generamba 30 | ``` 31 | 32 | ### Usage 33 | 34 | #### Setting up the project 35 | 36 | Before creating a module you need to configure the `Rambafile` by calling `generamba setup` (the file is located in the project’s root folder). It contains project name, prefix, company, module, paths to test folders and a list of used templates. The file is versioned in git and should be used by all developers maintaining the project. 37 | 38 | #### Creating a new module 39 | 40 | Generation of a new module is performed by calling `generamba gen ModuleName TemplateName`. As a result, it will create all files described in the given template - and they will be added to Xcode project, as well as to the file system. 41 | 42 | #### Working with templates 43 | 44 | All templates which are used in the current project is described in `Rambafile`. The call to `generamba template install` will install provided templates one by one - either from a local folder, from a remote git repository or even from template catalog([including shared ones](https://github.com/rambler-digital-solutions/generamba-catalog)). All templates are stored in the project’s `Templates` folder. 45 | 46 | The full list of commands and their options can be found in our [wiki](https://github.com/rambler-digital-solutions/Generamba). 47 | 48 | The project is open sourced, so everyone who is willing [can help us in development, send us bug reports and share your ideas](https://github.com/rambler-digital-solutions/Generamba/issues). Besides, we will be happy to add new templates to [our catalog](https://github.com/rambler-digital-solutions/generamba-catalog) via Pull Requests. 49 | 50 | ### Other code generators 51 | - [vipergen](https://github.com/teambox/viper-module-generator) 52 | - [boa](https://github.com/team-supercharge/boa) 53 | -------------------------------------------------------------------------------- /russian/rambler-materials.md: -------------------------------------------------------------------------------- 1 | # Дополнительные материалы Rambler&Co 2 | 3 | #### Rambler.iOS V - V is for VIPER 4 | 5 | В Rambler&Co периодически проводятся встречи iOS разработчиков. Одна из них была полностью посвящена VIPER - и стала основой для этой книги. 6 | 7 | - Вступление ([Видео](http://www.youtube.com/watch?v=zjw6Md1mMjQ)) - [Егор Толстой](https://github.com/igrekde) 8 | - VIPER a la Rambler ([Видео](http://www.youtube.com/watch?v=mEju4PyuCBM) | [Слайды](http://www.slideshare.net/Rambler-iOS/viper-a-la-rambler)) - [Сергей Крапивенский](https://github.com/serkrapiv) 9 | - Кодогенерация и Генерамба ([Видео](http://www.youtube.com/watch?v=NXNiN9FaUnY) | [Слайды](http://www.slideshare.net/Rambler-iOS/viper-56423582)) - [Егор Толстой](https://github.com/igrekde) 10 | - Переходы между модулями ([Видео](http://www.youtube.com/watch?v=XvAHqDvGqzE) | [Слайды](http://www.slideshare.net/Rambler-iOS/viper-56423732)) - [Вадим Смаль](https://github.com/CognitiveDisson) 11 | - Сложные модули ([Видео](http://www.youtube.com/watch?v=4ZPQ_qotx4M) | [Слайды](http://www.slideshare.net/Rambler-iOS/viper-56423837)) - [Андрей Зарембо](https://github.com/AndreyZarembo) 12 | - Разбиваем Massive View Controller ([Видео](http://www.youtube.com/watch?v=aVuIk6F2rFA) | [Слайды](http://www.slideshare.net/Rambler-iOS/massive-view-controller)) - [Александр Сычев](https://github.com/Brain89) 13 | - Тестирование VIPER ([Видео](http://www.youtube.com/watch?v=1y2vxtD7b6g) | [Слайды](http://www.slideshare.net/Rambler-iOS/tdd-viper)) -[Станислав Цыганов](https://github.com/AlloyDev) 14 | - VIPER и Swift ([Видео](http://www.youtube.com/watch?v=m4MYKzlqtH8) | [Слайды](http://www.slideshare.net/Rambler-iOS/viper-swift)) - [Валерий Попов](https://github.com/complexityclass) 15 | - Секция вопросов и ответов ([Видео](http://www.youtube.com/watch?v=mFvAIcL4C_4)) - [Егор Толстой](https://github.com/igrekde), [Сергей Крапивенский](https://github.com/serkrapiv) 16 | 17 | #### Rambler&IT 18 | 19 | Теоретические материалы - это отлично, но одной из главных проблем, с которой мы столкнулись при знакомстве с VIPER, это отсутствие примеров его применения в приложениях сложнее обычного *Hello World*. 20 | 21 | Мы постарались решить этот вопрос и выложили в Open Source приложение [Rambler&IT](https://github.com/rambler-digital-solutions/RamblerConferences). Его основные особенности: 22 | 23 | - Разбито на три основных слоя: `Presentation`, `BusinessLogic`, `Core`. 24 | - Слой `Presentation` целиком написан с использованием VIPER. 25 | - Слой `BusinessLogic` написан с использованием Service Oriented Architecture. 26 | - Слой `Core` написан с использованием концепции составных операций, вдохноновленной [сессией 226](https://developer.apple.com/videos/play/wwdc2015/226) WWDC 2015. 27 | - Для Dependency Injection активно используется Typhoon. 28 | 29 | Приложение получилось достаточно крупным и сложным и продолжает активно развиваться. На настоящий момент это наиболее полный открытый пример использования VIPER в боевом проекте. 30 | 31 | #### Другие конференции 32 | 33 | Помимо Rambler.iOS мы и на других конференциях рассказывали про использование VIPER. 34 | 35 | - VIPER - или то, о чем все говорят, но никто не рассказывает ([Видео](https://www.youtube.com/watch?v=dGTdlNjh_5U) | [Слайды](https://speakerdeck.com/etolstoy/viper-ili-to-o-chiem-vsie-ghovoriat-no-nikto-nie-rasskazyvaiet)) - [Егор Толстой](https://github.com/igrekde) 36 | - Чистая архитектура с VIPER ([Видео](https://www.youtube.com/watch?v=uS8zropcvdU) | [Слайды](http://www.slideshare.net/codefest/ss-60159923)) - [Сергей Крапивенский](https://github.com/serkrapiv) 37 | - VIPER: наш взгляд на вопрос ([Видео](https://www.youtube.com/watch?v=WY63iqXXtG4) | [Слайды](http://www.slideshare.net/uamobile/viper-ua-mobile-2016)) - [Екатерина Коровкина]() 38 | -------------------------------------------------------------------------------- /russian/code-generation.md: -------------------------------------------------------------------------------- 1 | Создание нового модуля - одно из самых узких мест в VIPER, особенно с точки зрения стороннего человека. Для того, чтобы создать новый модуль-экран, нужно как минимум: 2 | 3 | - **Пять новых классов** (Assembly, ViewController, Presenter, Interactor, Router), 4 | - **Пять новых протоколов** (ViewInput, ViewOutput, InteractorInput, InteractorOutput, RouterInput), 5 | - **Пять новых тестов** (AssemblyTests, ViewControllerTests, PresenterTests, InteractorTests, RouterTests). 6 | 7 | Кроме этого, нужно установить все необходимые связи, добавить реализацию протоколов, настроить dependency injection контейнер - и в частных случаях еще множество других действий. Такая сложность несет за собой две основные проблемы: 8 | 9 | - Слишком много времени уходит на простую механическую работу, 10 | - Повышается вероятность опечаток, которые могут повредить не только стилю кода, но и логике работы. 11 | 12 | Один из способов решения проблемы, которым долгое время пользовались и мы в Rambler&Co — это создание собственных шаблонов для Xcode. Такой подход решает все обозначенные вопросы, но имеет ряд собственных недостатков: 13 | 14 | - Создание нового темплейта - сложный процесс из-за достаточно громоздкого синтаксиса, 15 | - Периодически при обновлении Xcode темплейты могут слететь, 16 | - Нет удобного механизма добавления новых шаблонов в Xcode (Alcatraz не в счет), 17 | - Принципиально отсутствует возможность добавления файлов шаблона в разные таргеты (к примеру, при автогенерации тестов), 18 | - Настройка шаблонов и параметров кодогенерации во многом ориентирована на конкретного пользователя, а не на проект. 19 | 20 | Чтобы не быть ограниченными деталями реализации и работы нашей IDE, мы решили вынести процесс кодогенерации на другой уровень и написали небольшую утилиту - [Generamba](https://github.com/rambler-digital-solutions/Generamba). 21 | 22 | ![Generamba](http://s24.postimg.org/gej9cg1cl/generamba.jpg) 23 | 24 | --- 25 | 26 | ### Установка 27 | 28 | ``` 29 | gem install generamba 30 | ``` 31 | 32 | ### Функциональность 33 | 34 | **Настройка параметров проекта** 35 | 36 | Все параметры, нужные для кодогенерации, содержатся в основной папке проекта в файле `Rambafile`. Настраивается он полуавтоматически в результате вызова команды `generamba setup`. В нем находятся такие данные, как название проекта, префикс, компания, пути до папок модулей и тестов, перечисление используемых шаблонов. Этот файл держится в git и используется всеми разработчиками проекта. 37 | 38 | **Генерация нового модуля** 39 | 40 | Создание шаблона осуществляется командой `generamba gen ModuleName TemplateName`. В результате будут созданы все файлы, описанные в конкретном шаблоне - причем добавятся в Xcode, так и в файловую систему. 41 | 42 | **Работа с шаблонами** 43 | 44 | Все шаблоны, используемые в текущем проекте, описываются в `Rambafile`. При запуске команды `generamba template install` поочередно устанавливается каждый из указанных шаблонов - либо из локальной папки, либо из удаленного git-репозитория, либо из каталогов шаблонов ([в том числе и общего](https://github.com/rambler-digital-solutions/generamba-catalog)). Все шаблоны хранятся в папке `Templates` текущего проекта. 45 | 46 | С полным списком команд и их опций можно ознакомиться в нашей [wiki](https://github.com/rambler-digital-solutions/Generamba/wiki/Available-Commands). 47 | 48 | Проект выложен в open source, поэтому каждый желающий [может помочь нам в его развитии, сообщить об ошибках и оставить свои идеи](https://github.com/rambler-digital-solutions/Generamba/issues). 49 | 50 | Кроме того, мы с радостью добавляем новые шаблоны в [наш каталог](https://github.com/rambler-digital-solutions/generamba-catalog) через Pull Request'ы. 51 | 52 | ### Другие кодогенераторы 53 | - [vipergen](https://github.com/teambox/viper-module-generator) 54 | - [boa](https://github.com/team-supercharge/boa) 55 | -------------------------------------------------------------------------------- /russian/module-transitions.md: -------------------------------------------------------------------------------- 1 | ## Переходы между модулями - введение 2 | 3 | Подход к созданию модулей через фабрику, описываемый в статьях про канонический VIPER, достаточно неудобен. Мы решили попробовать родные `UIStoryboardSegue` для переходов между модулями. Такой подход открывал заманчивые перспективы - ведь для перехода в другой модуль необходимо было бы всего лишь указать SegueID и передать в модуль данные для работы. Кроме того, для конфигурации модулей мы используем Typhoon, поэтому все модульные ViewController после инициализации через Segue уже имеют связи с другими компонентами модуля. 4 | 5 | ## Переходы между модулями - через ViewController 6 | 7 | Это самый простой вариант. У роутера вызывающего модуля есть ссылка на свой ViewController, при переходе на другой модуль у ViewController вызывается метод `-prepareForSegue:`, где в `sender` передаются данные для следующего модуля. Внутри `-prepareForSegue:` вызывающего ViewController эти данные передаются в следующий модуль. 8 | 9 | Такой подход работает, но есть и некоторые недостатки: 10 | - Логика настройки следующего модуля размещается внутри View, а не в Router, 11 | - Нет универсальности и переиспользования, этот метод нужно реализовывать в каждом модуле, 12 | - Данные для работы следующего модуля попадают во View, а не в Presenter, 13 | - Каждый модуль знает об устройстве другого модуля, 14 | - Каждый роутер знает, что работает с классом `UIViewController`, и схема работает только для этого варианта. 15 | 16 | ## Переходы между модулями - через ViewController c блоком конфигурации 17 | 18 | Для решения первых двух проблем были использованы method-swizzling и блоки. В `-prepareForSegue:` в `sender` отправляется блок, в котором выполяется настройка модуля через `destinationViewController`. В альтернативном методе `-prepareForSegue:` блок вызывается с destinationViewController из segue в качестве параметра. 19 | 20 | Это работает, логика настройки следующего модуля находится целиком внутри Router, для каждого модуля больше не требуется добавлять во ViewController метод `-prepareForSegue:`, но остаются три проблемы: 21 | 22 | - Данные для работы следующего модуля попадают во View, а не в Presenter, 23 | - Каждый модуль знает об устройстве другого модуля, 24 | - Каждый роутер знает, что работает с ViewController и схема работает только для этого. 25 | 26 | ## Переходы между модулями - много протоколов 27 | 28 | Чтобы решить оставшиеся проблемы были использованы протоколы. Много протоколов. А также swizzling и своя реализация promise. В итоге получилась система передачи данных между модулями без перечисленных недостатков, данные из презентера отдаются роутеру и он конфигурирует ими презентер следующего модуля. Но появились две новые проблемы: 29 | - На освоение у нового разработчика уходило порядка 2х дней, 30 | - Данные передавались только в одну сторону. 31 | 32 | ## Переходы между модулями - вариант с ModuleInput 33 | 34 | Текущий вариант, доступный в нашем Github под названием [ViperMcFlurry](https://github.com/rambler-digital-solutions/ViperMcFlurry) стал гораздо проще в освоении. У каждого модуля теперь есть точка входа - ModuleInput, которая позволяет настроить модуль или вызывать методы. Этот moduleInput можно использовать внутри роутера для настройки модуля, можно вернуть презентеру, для постоянной связи с подмодулем. 35 | У каждого модуля можно задать ModuleOutput, чтобы вернуть данные из модуля. ModuleInput/Output - это протоколы, которые задаются внутри модуля, то есть в них хранится контракт связи с ним. В большинстве модулей в роли ModuleInput выступает презентер этого модуля, а в качестве ModuleOutput - презентер вызывающего модуля. 36 | 37 | ## Embed Segue 38 | 39 | Поскольку для метода `-performSegue` требуется только имя перехода, `-prepareForSegue:` подвергся swizzle'ингу, а Typhoon настраивает модуль по ViewController, то мы можем использовать любые классы Segue и механизм переходов между модулями будет работать. 40 | 41 | Поэтому для встраивания модулей был создан специальный тип `UIStoryboardSegue` EmbedSegue. Внутри `-performSegue` у SourceViewController вызывается метод, который возвращает View для идентификатора Segue. В эту View и встраивается модуль. 42 | -------------------------------------------------------------------------------- /english/other-materials.md: -------------------------------------------------------------------------------- 1 | ### Foreword 2 | 3 | There are lots of articles, presentations and videos related to VIPER in the Internet. Some of them are good and helpful, some are harmful. But it's better to study most of them. 4 | 5 | **Important notice:** Materials in this list doesn't mean that we agree with authors and endorse it's ideas. It's possible that we just like fonts and found some jokes funny. 6 | 7 | ### Articles 8 | - [objc.io #13 - Architecting iOS Apps with VIPER](http://www.objc.io/issues/13-architecture/viper/) 9 | 10 | **Authors:** Jeff Gilbert, Conrad Stoll. 11 | 12 | **Review:** *Unfading classics you should already read. Two things makes it famous: Firstly it was written it times of true objc.io, secondly it was written by the author of VIPER Jeff Gilbert.* 13 | 14 | *VIPER realization in this article should not be taken seriously. It's not optimal and buggy in some cases. But it has example projects on ObjC and SWIFT :)* 15 | 16 | - [Introduction to VIPER](http://mutualmobile.github.io/blog/2013/12/04/viper-introduction/) 17 | 18 | **Author:** Jeff Gilbert. 19 | 20 | **Review:** *Another must-read article, MutualMobile tells about their way to VIPER. You should pay attention to first paragraphs. There Jeff tells that use of such architecture was forced by need of UI tests. Also there's nice point about naming - first VIP letters and think out of E and R.* 21 | 22 | *But we're not agree with some points. Especially with Wireframe concepts, total prohibition of ManagedObject use out of Interactor and direct use of DataStore.* 23 | 24 | - [Brigade’s Experience Using an MVC Alternative](https://medium.com/brigade-engineering/brigades-experience-using-an-mvc-alternative-36ef1601a41f) 25 | 26 | **Author:** Ryan Quan. 27 | 28 | **Review:** *On our opinion it's highest bidder to role of "The best introduction to VIPER". Good test, simple schemes, clear description of main ideas and principles. The one from popular tutorials, that advises to move business logic into service layer. This article is recomended at motivation material for your team, family and friends.* 29 | 30 | *Of course, here we have Wireframe. Moreover, move of DataManager(we call it ServiceFacade) from Interactor is so rare case, to advice it usage in most modules.* 31 | 32 | - [The Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) 33 | 34 | **Authors:** Robert Martin. 35 | 36 | **Review:** *It's not about VIPER itself, but definitely must read. Uncle Bob talks about clean arcitecture and DI, draws circles and do lots of interesting things.* 37 | 38 | *It's hardly to use such architecture in our harsh reality, but it was exactly the thing that lead MutualMobile to create VIPER.* 39 | 40 | ### Podcasts 41 | - [iPhreaks Show - VIPER](https://itunes.apple.com/ru/podcast/the-iphreaks-show/id634022060?mt=2&i=316803444) 42 | 43 | **Participants:** Conrad Stoll, Jeff Gilbert. 44 | 45 | **Review:** *Many things could be different if ideas from this podcast were told in the Objc.io article. Authors of VIPER tells about their motivation, refactoring approaches, creation of composite screens, testing and many other things. This podcast is one of the best things you can find about VIPER.* 46 | 47 | ### Videos 48 | - [250 Days Shipping With Swift and VIPER](https://realm.io/news/altconf-brice-pollock-250-days-shipping-with-swift-and-viper/) 49 | 50 | **Speaker:** Brice Pollock. 51 | 52 | **Review:** *Сheerfully, Funny, SWIFTy. Coursera developer tells about VIPER experience. The team was not satisfied with canonical model and appended ViewModel, EventHandler and FlowController to it. Looks interesting, but data flow inside module looks frighteningly.* 53 | 54 | - [Clean Architecture - VIPER](https://www.youtube.com/watch?v=OX4rLAJC7lw) 55 | 56 | **Speaker:** Sergi Gracia. 57 | 58 | **Review:** *Some information about elements responsibility, little more about testing, few words about SOLID and lots about Redbooth office and team. And great fonts in the keynote. Nothing special, just another one introduction into VIPER concept.* 59 | 60 | *The main claim is quality of the Video. It definitely should be seen with [slides in parallel](https://speakerdeck.com/sergigracia/clean-architecture-viper).* 61 | -------------------------------------------------------------------------------- /russian/testing.md: -------------------------------------------------------------------------------- 1 | ## TDD и VIPER 2 | 3 | ### Кратко о том что такое TDD 4 | 5 | TDD - разработка через тестирование. Предполагается, что в начале пишутся тесты, потом реализация. В случае, если мы закрываем реальные классы протоколами, они идут первыми. После того, как все компоненты написаны - они могут быть интегрированы. 6 | 7 | Благодаря такому подходу достигается полное описание ожидаемого поведения класса в тестах. Тесты могут служить в качестве примеров использования класса. Также TDD позволяет нам непредвзято посмотреть на класс с точки зрения пользователя данного класса до его написания. 8 | 9 | Более подробно тема TDD в iOS была разобрана в [статье](http://habrahabr.ru/company/rambler-co/blog/263087/) Андрея Резанова. 10 | 11 | ## VIPER 12 | Обычно разделения на основные слои приложения достаточно для того, чтобы довольно неплохо протестировать сервисный и Core слой, но при наличии "толстого" ViewController возникают проблемы в тестировании Presentation слоя. Поэтому многие оставляют его без достаточного покрытия тестами. Давайте разберемся, как VIPER-модули могут помочь нам в покрытии View тестами. 13 | 14 | Общим подходом для тестирования является следующий: окружаем объект тестируемого класса протокол-моками зависимостей. Вызываем методы интерфейса/манипулируем свойствами, проверяем вызовы методов моков. 15 | 16 | ### Тестирование View 17 | Существует некоторое количество библиотек, помогающих в тестировании UI слоя, а с недавних пор у нас появились еще и UI-тесты. Достаточно ли этого для полноценного покрытия View? На наш взгляд - нет. 18 | 19 | UI-тесты могут служить неплохим способом написания acceptance, или приемочных тестов. В роли черного ящика выступает все боевое приложение, и мы через интерфейс приложения пытаемся получить тот или иной результат. 20 | 21 | Проблема с UI-тестами в том, что в случае перегруженного VC весь View является для нас черным ящиком с множеством состояний, и для тестирования необходимо слишком большое количество тестов. 22 | 23 | Чем нам тут может помочь VIPER? Из ViewController выносится логика по подготовке и представлению данных в Presenter. Соответственно на View ложится ответственность лишь за обработку и прокидывание событий в Presenter, а также за отображение UI. Отдельно необходимо заострить внимание на том, что View - это не обязательно UI, а класс, через который происходит взаимодействие внешнего мира с модулем. Что это означает для нас? IBOutlet и IBAction **необходимо** делать публичными. Как итог мы получаем возможность протестировать всевозможные нажатия, заполнение полей и прочее без симуляции нажатий, поиска нужных кнопочек по тексту на них и прочих ненадежных вещей. 24 | 25 | Подытожим, выделив 2 основных вида тестов: 26 | 27 | - Взаимодействуем с IBOutlet/вызываем IBAction -> проверяем, что были вызваны соответствующием методы мока Presenter 28 | - Вызываем методы протокола, через который общается Presenter -> проверяем, что меняются IBOutlet/View 29 | 30 | Отдельно можно выделить тестирование методов жизненного цикла ViewController/View, на которые нам так или иначе необходимо ориентироваться, так как зачастую Presenter не может начинать настройку View до `-viewDidLoad` или `-viewWillAppear`. 31 | 32 | ### Тестирование Router 33 | Методы роутера вызываются, когда необходимо совершить переход из текущего ViewController. Соответственно тестами покрываются методы переходов/закрытия текущего контроллера. Тестируем вызовы всевозможных аниматоров переходов. 34 | 35 | ### Тестирование Interactor 36 | Interactor является связующим звеном в работе с всевозможными сервисами. Именно через него идет работа со Storage, в нем создаются PONSO-объекты. 37 | 38 | Большинство тестов касаются проверки вызовов одних сервисов в зависимости от ответов других. 39 | 40 | ### Тестирование Presenter 41 | Presenter можно назвать связующим звеном модуля, так как в нем происходит проксирование запросов от одной части модуля к другой. Тесты на подобную передачу составляют львиную долю от тестов Presenter. 42 | 43 | Отдельно стоит отметить, что Presenter является входной точкой для модуля, именно в него передаются данные с предыдущего контроллера. На это опять же необходимо писать тесты. 44 | 45 | Presenter является местом, где чаще всего находится максимальное число логических ветвлений. Это также необходимо учитывать, и на вход подавать все возможные принципиально отличающиеся друг от друга комбинации значений. 46 | 47 | ### Тестирование Assembly 48 | Assembly настраивает зависимости компонентов модуля. Ее тесты ответственны за то, чтобы проверять, что модуль состоит из правильных частей и все зависимости заполнены. 49 | 50 | К счастью, при строгой структуре модуля, данные тесты могут быть созданы автоматически. 51 | -------------------------------------------------------------------------------- /russian/other-materials.md: -------------------------------------------------------------------------------- 1 | ### Предисловие 2 | На просторах сети можно найти достаточно много различных статей, презентаций и видео, так или иначе связанных с VIPER. Не все из них одинаково полезны, некоторые даже вредны - но со многими ознакомиться не только можно, но и нужно. 3 | 4 | **Важно:** Наличие материала в этом списке не означает нашего молчаливого согласия со всеми изложенными в нем идеями. Возможно, нам просто понравились шуточки или шрифты. 5 | 6 | ### Статьи 7 | - [objc.io #13 - Architecting iOS Apps with VIPER](http://www.objc.io/issues/13-architecture/viper/) 8 | 9 | **Авторы:** Jeff Gilbert, Conrad Stoll. 10 | 11 | **Рецензия:** *Неувядающая классика, которую вы уже должны были прочесть. Знаменательна благодаря двум фактам: во-первых, это статья времен еще годного objc.io, во-вторых - авторство принадлежит автору идеи VIPER Jeff Gilbert.* 12 | 13 | *Как и любой из прочих материалов, не стоит принимать слишком всерьез - предлагаемая реализация в целом неоптимальна, а во многом и вообще ошибочна. Но зато есть тестовые проекты на ObjC и Swift.* 14 | 15 | - [Introduction to VIPER](http://mutualmobile.github.io/blog/2013/12/04/viper-introduction/) 16 | 17 | **Авторы:** Jeff Gilbert. 18 | 19 | **Рецензия:** *Еще один must-read, MutualMobile рассказывают о том, как они докатились до VIPER. Особенное внимание стоит обратить на первые абзацы, где Jeff говорит о том, что к необходимости использования такой архитектуры их подвела потребность в покрытии UI тестами. Неплохой вброс и про историю появления названия - про первоначальные буквы VIP и додумывание E и R.* 20 | 21 | *Тем не менее, с некоторыми позициями мы не согласны - в том числе с концепцией Wireframe, тотальным запретом на передачу ManagedObject'ов выше интерактора и прямым использованием DataStore.* 22 | 23 | - [Brigade’s Experience Using an MVC Alternative](https://medium.com/brigade-engineering/brigades-experience-using-an-mvc-alternative-36ef1601a41f) 24 | 25 | **Авторы:** Ryan Quan. 26 | 27 | **Рецензия:** *На наш взгляд, это главный претендент на роль лучшего вступления в VIPER. Хороший язык, простые схемы, четкое объяснение основных идей и принципов. Единственные (из популярных туториалов) рекомендуют выносить бизнес-логику в сервисный слой. Рекомендуется использование в качестве мотивационного материала для своей команды, семьи и друзей.* 28 | 29 | *Конечно, здесь нас снова ожидает наш старый знакомый - Wireframe. Кроме того, выделение DataManager'а (а мы его называем ServiceFacade) из интерактора - это достаточно редкий кейс, чтобы рекомендовать его для использования на постоянной основе во всех модулях.* 30 | 31 | - [The Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) 32 | 33 | **Авторы:** Robert Martin. 34 | 35 | **Рецензия:** *Хоть и не напрямую относится к VIPER, но однозначно достойно прочтения. Дядюшка Боб раскрывает всем глаза на то, что такое чистая архитектура, рисует кружочки, говорит про DI и делает кучу других интересных вещей.* 36 | 37 | *Переложить архитектуру в чистом виде на нашу суровую реальность вряд ли получится - но именно идеи из этой статьи послужили для MutualMobile толчком к VIPER.* 38 | 39 | - [iOS Architecture Patterns](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.pbidzgzgy) 40 | 41 | **Авторы:** Bohdan Orlov 42 | 43 | **Рецензия:** *Отличный материал, в котором по полочкам разложены особенности MVC, MVVM, MVP и VIPER. Красивые схемки, четко изложенные плюсы и минусы каждого из подходов, есть даже ссылка на нашу горячо любимую Генерамбу.* 44 | 45 | *Из минусов - как всегда, рассматриваются слишком утрированные примеры из пары десятков строк, на которых достаточно тяжело увидеть как потенциальную пользу, так и сложности в использовании.* 46 | 47 | ### Подкасты 48 | - [iPhreaks Show - VIPER](https://itunes.apple.com/ru/podcast/the-iphreaks-show/id634022060?mt=2&i=316803444) 49 | 50 | **Участники:** Conrad Stoll, Jeff Gilbert. 51 | 52 | **Рецензия:** *Если бы все то, о чем говорится в этом подкасте, было упомянуто в `той самой` статье на objc.io - многое могло пойти по-другому. Создатели VIPER подробно рассказывают о своей мотивации, подходах к рефакторингу, реализации сложных композитных экранов, тестировании, и многом другом. Незаслуженно пропущенный широким кругом iOS-разработчиков, этот подкаст - чуть ли не лучшее из того, что можно прочитать/увидеть/услышать относительно VIPER.* 53 | 54 | ### Видео 55 | - [250 Days Shipping With Swift and VIPER](https://realm.io/news/altconf-brice-pollock-250-days-shipping-with-swift-and-viper/) 56 | 57 | **Докладчик:** Brice Pollock. 58 | 59 | **Рецензия:** *Бодро, весело, про свифт. Разработчик из Coursera рассказывает об их опыте работы с VIPER. Как и нас, ребят не удовлетворила каноничная модель, и они своими силами расширили ее, включив туда ViewModel, EventHandler, FlowController. Выглядит интересно, но схема обмена данными в рамках одного модуля на 12 минуте вызывает благоговейный ужас.* 60 | 61 | - [Clean Architecture - VIPER](https://www.youtube.com/watch?v=OX4rLAJC7lw) 62 | 63 | **Докладчик:** Sergi Gracia. 64 | 65 | **Рецензия:** *Немного про ответственности элементов, немного про тестирование, немного про SOLID, много про офис и команду Redbooth - даже с элементами воркшопа. А шрифты в презентации - просто огонь. Ничего необычного - просто еще одно введение в концепцию VIPER.* 66 | 67 | *А главная претензия - очень плохое качество видео, поэтому однозначно параллельно стоит посмотреть [сопутствующие слайды](https://speakerdeck.com/sergigracia/clean-architecture-viper).* 68 | -------------------------------------------------------------------------------- /english/module-structure.md: -------------------------------------------------------------------------------- 1 | ## Application Modularization 2 | 3 | Any project can be divided into several logical parts with clearly defined functionality. In Instagram app, the main part of the application - news feed, in which we can view the photos, navigate to the screen to create a comment or send a message. The next tab is a list of likes, from which we can move on to a particular photo. Each of these screens is a self-contained part of the application that performs the task well, and knowing how to initiate the transition to the other screens if necessary. We call these kinds of elements to modules. 4 | 5 | In the case of Instagram app, especially Profile screen, they are the photos module (1), information about the user module (2), the tabs for switching the photos list to other modules (3). 6 | 7 | ![Profile Instagram](../Resources/instagram_example_serkrapiv.png) 8 | 9 | ## VIPER-module structure 10 | 11 | In order to perform its mission module, it is necessary to solve several problems. It is required to implement the business logic module, networking, database, render the user interface. VIPER describes the role of each component and how they interact with each other. So, VIPER-module consists of the following components: 12 | 13 | - **View:** responsible for displaying data on the screen and notifies the **Presenter** of the user's actions. But **View** never asks for data. It just gets them from the presenter. 14 | 15 | - **Interactor:** contains all the business logic required for this module. 16 | 17 | - **Presenter:** receives information about the user's actions from **View** and transforms it into requests to **Router**, **Interactor**. Receives data from **Interactor**, prepares them and sends **View** to display. 18 | 19 | - **Entity**: model objects that do not contain any business logic. 20 | 21 | - **Router**: responsible for the navigation between the modules. 22 | 23 | ## What we have changed 24 | 25 | VIPER appears in its original form on [Mutual Mobile](https://www.objc.io/issues/13-architecture/viper/). We have worked with this approach, and soon realized that it has a few problems: 26 | 27 | 1) In the original version of the VIPER, **Wireframe** is responsible for **Routing** component. But it is also responsible for assembly of the module, the transition to which it carries out, and putting down all the dependencies in the module. This is bad because it violates the principle of [the Single Responsibility](https://en.wikipedia.org/wiki/Single_responsibility_principle). 28 | 29 | So, we decide to split into two parts of **Wireframe**. First, **Router** is responsible only for the transitions between the modules. Second, **Assembly** is responsible for affixing the module assembly and dependencies between all its components. In our projects, we use [Typhoon](https://github.com/appsquickly/Typhoon) for this purpose, a great library for Dependency Injection, and manually **Assembly** of the code is not even called. 30 | 31 | 2) **Interactor** hides a business logic. It sounds pretty scary, because behind it lies a lot of work, which is split between the specialized classes. Moreover, the same code is often reused within **Interactor**. We needed a common understanding of how to do it so as not to reinvent the wheel in each project. 32 | 33 | We decide to introduce an additional layer of services. **Service** - the object responsible for the work with your specific type of model objects. For example, a news service for the news corresponds to a specific category list, as well as detailed information about each news. Authorization service is responsible for, in fact, authorization, password recovery, update session and so on. In **Service**, in turn, it has dependencies on lower-level objects, responsible for working with the network or database. 34 | 35 | **Services** are injected into the **Interactor**. As a result, **Interactor** mainly serves as a facade, interact with the service and sends the resulting data to **Presenters**. We as a team have agreed that we have not go to levels above **Interactor** when using Core Data NSManagedObject. Therefore **Interactor** also occurs NSManagedObject conversion to Plain Old NSObject, then there is a simple NSObject. 36 | 37 | 3) In the VIPER unit as **View** often acts UIViewController. And sometimes in the controller contains code that is not directly related to the tasks of View. For example, working with tables and collections. These objects are created with protocols. So, it is intended that their implementation should be moved to separate object. But, in the example of the Mutual Mobile code, tables are in the UIViewController directly. 38 | 39 | We don't like it, and we decide that the View is in general not a single object and layer. In addition to the controller, this layer may contain additional objects, which are injected into the controller and take on some of his work. An example of such an object in our projects is DataDisplayManager, implements and methods UITableViewDatasource UITableViewDelegate, or their codes for collections. 40 | 41 | 4) data transmission between the modules is not covered in VIPER original. Our solution to this problem is described in the chapter [the transitions between the modules](ModuleTransitions.md) details but do not dwell on it. It is enough to say that each module can be input and output interfaces - ModuleInput protocols and ModuleOutput. The former is responsible for transferring the input data, such as article identifier for displaying articles. The latter is responsible for transmitting the result of the module. For example, we can send out the item selected by the user when working with the menu settings module. 42 | 43 | ## The final module circuit 44 | 45 | To illustrate all of the above, we suggest to familiarize with the final scheme VIPER unit. Don't be afraid. Not every module must contain a number of objects. The purpose of this scheme is the most fully displaying our approach to architecture as an example of a complex module. 46 | 47 | ![VIPER Driving Module](../Resources/module_structure.png) 48 | -------------------------------------------------------------------------------- /russian/module-structure.md: -------------------------------------------------------------------------------- 1 | ## Разбиение приложения на модули 2 | 3 | Любой проект можно разбить на несколько логических частей с четко определенной функциональностью. Возьмем, к примеру, Instagram. Основная часть приложения - лента новостей, в которой мы можем просматривать фотографии, ставить лайки, переходить к экрану создания комментария или к отправке сообщений. На другой вкладке находится список лайков, из которого мы можем переходить к конкретной фотографии. Каждый из этих экранов является самодостаточным элементом приложения, выполняющим четко поставленную задачу, и умеющим при необходимости инициировать переход на другие экраны. Такие элементы приложения мы называем модулями. 4 | 5 | В простых случаях один экран соответствует одному модулю, но могут быть и сложные экраны, на которых одновременно находятся несколько модулей. Пример из того же Instagram - экран профиля. На нем можно выделить модуль фотографий (1), модуль информации о пользователе (2), вкладки для переключения списка фотографий на другие модули (3). 6 | 7 | ![Профиль Instagram](../Resources/instagram_example_serkrapiv.png) 8 | 9 | ## Структура VIPER-модуля 10 | 11 | Для того, чтобы модуль выполнял свое предназначение, нужно решить ряд задач. Требуется реализовать бизнес-логику модуля, работу с сетью, базой данных, отрисовать пользовательский интерфейс. За все это должны отвечать отдельные компоненты, и VIPER описывает роль каждого и способы их взаимодействия между собой. Итак, VIPER-модуль состоит из следующих частей: 12 | 13 | **View:** отвечает за отображение данных на экране и оповещает Presenter о действиях пользователя. Пассивен, сам никогда не запрашивает данные, только получает их от презентера. 14 | 15 | **Interactor:** содержит всю бизнес-логику, необходимую для работы текущего модуля. 16 | 17 | **Presenter:** получает от **View** информацию о действиях пользователя и преображает ее в запросы к **Router’у**, **Interactor’у**, а также получает данные от **Interactor’a**, подготавливает их и отправляет **View** для отображения. 18 | 19 | **Entity**: объекты модели, не содержащие никакой бизнес-логики. 20 | 21 | **Router**: отвечает за навигацию между модулями. 22 | 23 | ## Что мы изменили 24 | 25 | Так VIPER выглядит в своем первозданном виде [от Mutual Mobile](https://www.objc.io/issues/13-architecture/viper/). Мы поработали с этим подходом и вскоре поняли, что в нем есть несколько не слишком удобных моментов: 26 | 27 | 1) В первоначальной версии VIPER за роутинг отвечает компонент под названием Wireframe. Но при этом он же отвечает за сборку модуля, переход к которому он осуществляет, и проставление всех зависимостей у этого модуля. Это плохо, поскольку нарушает принцип [Single Responsibility](https://en.wikipedia.org/wiki/Single_responsibility_principle). 28 | 29 | Мы решили разделить Wireframe на две части. Первая, Router, отвечает только за переходы между модулями. Вторая, Assembly, отвечает за сборку модуля и проставление зависимостей между всеми его компонентами. В наших проектах для этого используется [Typhoon](https://github.com/appsquickly/Typhoon), замечательная библиотека для Dependency Injection, благодаря использованию которой вручную Assembly из кода не вызывается. 30 | 31 | 2) Интеракторы скрывают в себе бизнес-логику. Звучит довольно страшно, ведь за этим кроется много работы, которую стоит разделять между специализированными классами. К тому же часто один и тот же код нужно переиспользовать в нескольких интеракторах. Нам было нужно общее понимание того, как это делать, чтобы не изобретать свои велосипеды в каждом проекте. 32 | 33 | Мы решили ввести дополнительный слой сервисов. Сервис - объект, отвечающий за работу со своим определенным типом модельных объектов. Например, сервис новостей отвечает за получение списка новостей в определенной категории, а так же подробной информации о каждой новости. Сервис авторизации отвечает за, собственно, авторизацию, восстановление пароля, обновление сессии и так далее. У сервисов, в свою очередь, есть зависимости на объекты нижнего уровня, отвечающие за работу с сетью или базой данных. 34 | 35 | Сервисы инжектируются в интерактор. В итоге интерактор в основном служит фасадом, взаимодействующим с сервисами и передающим полученные от них данные презентеру. Мы в команде договорились о том, что при работе с Core Data NSManagedObject’ы не выходят на уровни выше интерактора. Поэтому в интеракторах также происходит преобразование `NSManagedObject` в Plain Old NSObject, то есть простой наследник `NSObject`. 36 | 37 | 3) В модуле VIPER в качестве View чаще всего выступает `UIViewController`. А в контроллере иногда содержится код, не относящийся напрямую к задачам View. Пример: работа с таблицами и коллекциями. Не зря ведь для работы с этими объектами созданы протоколы, подразумевается, что их реализация должна быть вынесена в отдельный объект. Но в примере от Mutual Mobile код по работе с таблицами содержится прямо в `UIViewController`. 38 | 39 | Нам это не понравилось, и мы решили, что View в общем случае является не одним объектом, а слоем. Помимо контроллера этот слой может содержать дополнительные объекты, которые инжектируются в контроллер и берут на себя часть его работы. Примером такого объекта в наших проектах является DataDisplayManager, реализующий методы `UITableViewDataSource` и `UITableViewDelegate`, или их аналоги для коллекций. 40 | 41 | 4) Вопрос передачи данных между модулями в оригинальном VIPER не охвачен. Наше решение этой проблемы описано в главе “Переходы между модулями”, подробно на нем останавливаться не будем. Скажем лишь, что у каждого модуля могут быть интерфейсы входа и выхода - протоколы ModuleInput и ModuleOutput. Первый отвечает за передачу входных данных, например идентификатора статьи для экрана, отвечающего за отображение статьи. Второй отвечает за передачу результата работы модуля заинтересованному объекту. Например, при работе с модулем настроек мы можем передать наружу пункт меню, выбранный пользователем. 42 | 43 | ## Итоговая схема модуля 44 | 45 | Для иллюстрации всего описанного выше предлагаем ознакомиться с итоговой схемой модуля VIPER. Пугаться не стоит, далеко не каждый модуль должен содержать такое количество объектов. Целью этой схемы было максимально полно отобразить наш подход к архитектуре на примере сложного модуля. 46 | 47 | ![Схема модуля VIPER](../Resources/module_structure.png) 48 | -------------------------------------------------------------------------------- /russian/compound-modules.md: -------------------------------------------------------------------------------- 1 | ## Простой vs Сложный модуль 2 | 3 | Когда мы начинали использовать VIPER в своей работе, использовалась концепция "Один экран - один модуль". Это прекрасно работало, потому что экраны в основном представляли собой простые таблицы. Но с появлением первого "сложного" экрана начались проблемы. 4 | 5 | ## Примеры сложных модулей из почты - экран настроек и просмотр письма 6 | ![submodules.001](../Resources/submodules/submodules.001.png) 7 | Сложные экраны бывают разными. Например, экран настроек управляет множеством не связанных между собой элементов. 8 | Экран просмотра сообщения внутри себя содержит шапку, коллекции для контактов, вложений, а также просмотр письма, для которого необходимо применить специальные преобразования. 9 | 10 | ## Проблемы сложных модулей 11 | ![submodules.002](../Resources/submodules/submodules.002.png) 12 | Какие проблемы создают сложные модули? 13 | - Разнородные данные в одном модуле 14 | - Сложная логика работы 15 | - Затруднено тестирование 16 | - Невозможность переиспользования 17 | - Затруднено изменение функций и конфигурации 18 | 19 | Например модуль настроек: он должен хранить информацию об имени с подписью, список подключенных ящиков, статус уведомлений. Каждая секция настроек может влиять на своих соседей по секции, но, теоретически, не должна трогать остальные. Хотя такая возможность у неё есть. Переиспользовать такой модуль не получится, все заточено под опции конкретного приложения. Добавление или изменение настроек требует изучения работы всего модуля. 20 | 21 | ## Разбиение настроек и просмотра письма на подмодули 22 | ![submodules.003](../Resources/submodules/submodules.003.png) 23 | Достаточно очевидное разбиение настроек на подмодули - по секциям. Это логично и наглядно. Первый модуль - данные пользователя, второй модуль - список подключенных ящиков и так далее. 24 | 25 | С модулем просмотра сообщения все тоже достаточно просто - шапка, контакты, вложения (а они ещё и сворачиваются) и просмотр тела письма. 26 | 27 | ## Подмодули: плюсы и минусы 28 | 29 | ### Плюсы подмодулей: 30 | - Единая ответственность - каждый модуль может, а в идеале даже должен, отвечать за какую-то одну функцию. 31 | - Тестируемость - маленькие модули легче тестировать. 32 | - Переиспользуемость - модуль контактов или вложений может использоваться на экранах написания письма и даже в другом приложении, например мессенджере. 33 | - Для добавления новой функции можно добавить новый подмодуль. 34 | - Возможность создавать разные конфигурации, например добавить дополнительный пункт настроек только для разработчиков. 35 | 36 | ### Минусы: 37 | - Дополнительный код. Он обеспечивает инициализацию и согласованную работу подмодулей. Его требуется хорошо протестировать. 38 | - Опасность избыточного разделения. Система, разбитая на слишком маленькие модули, рассыпается. К примеру, в настройках не стоит делать модуль для каждого пункта. 39 | - Усложнение потоков данных. Если подмодуль подмодуля должен вернуть какие-то данные в основной модуль, цепочка вызовов будет выглядеть гораздо сложнее, чем в случае монолитного модуля. 40 | 41 | Поэтому разбиение на подмодули требует хорошего анализа. 42 | 43 | ## Варианты разделения сложных модулей на подмодули - обзор 44 | ![submodules.004](../Resources/submodules/submodules.004.png) 45 | В работе над проектами были использованы 4 варианта композитных модуля: 46 | - Модуль-контейнер 47 | - Scroll View Container 48 | - Таблица с группами ячеек 49 | - Таблица с ячейками-модулями 50 | - Модуль-View 51 | 52 | Рассмотрим их подробнее 53 | 54 | ## Модуль-контейнер 55 | ![submodules.005](../Resources/submodules/submodules.005.png) 56 | Это аналог Container и EmbedSegue, но управляемый из модуля. Презентер просит роутер добавить дочерний модуль. Роутер инициализирует дочерний модуль, отдает ему данные для работы, добавляет ViewController подмодуля как дочерний для контроллера модуля. И аналогично View контроллера подмодуля добавляется во View-контейнер контроллера модуля. 57 | 58 | Такой подход хорошо использовать, когда требуется отдельная логика работы внутри модуля, например для таблиц, у которых есть сложная шапка. 59 | 60 | ## Модуль-контейнер - пример 61 | Представьте список постов пользователя, над ними шапка с аватаркой и возможностью написать ему сообщение. 62 | Весь модуль постов занимается только постами, он их загружает, отображает и обрабатывает нажание на ячейку поста с переходом на просмотр поста. Если сюда добавить ещё и загрузку профиля с переходом на написание сообщения, модуль заметно усложнится, поэтому их удобно вынести в отдельный встраиваемый подмодуль. Для работы ему требуется только идентификатор пользователя. 63 | 64 | ## Scroll View Controller 65 | ![submodules.006](../Resources/submodules/submodules.006.png) 66 | Сложный случай модуля-контейнера. Так реализован экран просмотра сообщений в почтовом клиенте Рамблер/почта. Внутри Scroll View находится несколько контейнеров, каждый из них независимо управляется своим подмодулем. Модуль контактов работает только с загрузкой и отображением контактов, отвечает за сокрытие/разворачивание списка. Модуль вложений разделяет вложения на картинки и документы, отвечает за скрытие и разворачивание списка. Модуль отображения сообщения обрабатывает письма, добавляет переносы в длинные строки, загружает и кеширует inline вложения с картинками. 67 | 68 | Причем, как и положено в VIPER, все связанное с загрузкой, обработкой и бизнес-логикой выполяют интеракторы подмодулей. Для этого у них есть ссылки на сервисы. Технически, каждый такой подмодуль можно развернуть на весь экран. 69 | 70 | ## Таблица с группами ячеек 71 | ![submodules.007](../Resources/submodules/submodules.007.png) 72 | Это способ построения таблицы настроек. Интерактору при инициализации отдается список подмодулей для отображения. Он опрашивает каждый подмодуль и асинхронно получает массив view-model для каждого подмодуля, склеивает их в общий массив и передает своей таблице для отображения. Фабрика ячеек из cell-model получает все необходимые данные для создания и конфигурации ячейки, поэтому универсальна для всех подмодулей. 73 | 74 | У подмодулей в качестве View используются фабрики cell-model, они преобразуют данные от presenter в подходящий для отображения в виде ячеек вид, транслируют события из ячеек в presenter, то есть полностью выполняют всю работу View. 75 | 76 | Это позволяет модулю уведомлений работать только с уведомлениями, а модулю подключенных ящиков загрузить список ящиков из своего сервиса для отображения. 77 | 78 | ## Таблица с ячейками-модулями 79 | ![submodules.008](../Resources/submodules/submodules.008.png) 80 | Бывают ситуации, когда отображаемый в таблице контент очень сложный, в ячейке обрабатывается много действий пользователя, ей для работы требуется загружать дополнительные данные или нужно отображать внутри себя CollectionView. В таком случае можно сделать каждую ячейку отдельным модулем. 81 | 82 | Сложность в том, что необходимо сделать переиспользуемыми не только ячейки, но и модули VIPER. Фабрика ячеек должна настраивать состояние модуля при отображении ячейки. Например в случае с CollectionView внутри ячейки нужно передать ей не только список объектов для отображения, но и задать соответствующий ContentOffset. 83 | 84 | Зато это позволяет обрабатывать все действия с события внутри такого подмодуля, в том числе связываться с сервером. 85 | 86 | ## Модуль View 87 | ![submodules.009](../Resources/submodules/submodules.009.png) 88 | Самый очевидный способ - модуль View. Такой подход хорошо интегрируется с другими вариантами. Например, у нас может быть модуль ячейки, внутри которой есть подмодуль галереи или видео плеера. Такие подмодули могут быть легко переиспользуемы на других экранах. 89 | 90 | ## Когда подмодули помогают? 91 | ![submodules.010](../Resources/submodules/submodules.010.png) 92 | - Уменьшает сложность основного модуля 93 | - Легкое переиспользование подмодулей 94 | - Упрощает добавление новой функциональности 95 | - Упрощает тестирование 96 | 97 | ## Когда подмодули мешают? 98 | ![submodules.011](../Resources/submodules/submodules.011.png) 99 | - Значительно увеличивает объем кода 100 | - Усложняет логику 101 | - Усложняет отладку 102 | - Тяжело поддерживать 103 | -------------------------------------------------------------------------------- /russian/webview.md: -------------------------------------------------------------------------------- 1 | ### UIWebView в VIPER 2 | 3 | Основная идея - разбить ответственности `UIWebView` на две группы: `WebEngine`, с которым работает Интерактор, и `WebPresentation`, с которым работает View. 4 | 5 | #### Ответственности WebEngine 6 | - Загрузка HTML. 7 | - Исполнение JavaScript скриптов. 8 | - Реализация кастомной стратегии рендеринга контента. 9 | - Уведомление Интерактора о событиях окончания отрисовки, загрузки и рендеринга. 10 | 11 | #### Ответственности WebPresentation 12 | - Уведомление View о различных пользовательских действиях: нажатии на ссылки, изображения, видео. 13 | - Предоставление интерфейса для получения информации о `UIWebView` как об объекте отображения, к примеру, размера ее контента. 14 | 15 | ![Module Scheme](../Resources/webview-scheme.png) 16 | 17 | #### Логика работы модуля 18 | 19 | 1. Assembly устанавливает двух делегатов для `WebEngineImplementation` - View и Interactor. 20 | 2. Presenter (или View, в случае ячейки) является входной точкой модуля, в которую приходит сырой html. 21 | 3. Presenter получает `WebEngine` у View и передает его в Interactor. 22 | 4. Presenter передает в Interactor html данные. 23 | 5. Interactor настраивает `WebEngine` путем передачи ему определенных скриптов на выполнение. 24 | 6. Interactor передает html в `WebEngine`. 25 | 7. `WebEngine` сообщает Interactor'у обо всех этапах загрузки данных. 26 | 8. Interactor передает эти callback'и в Presenter, который реализует остальную логику. 27 | 28 | Каждый из этих шагов очень прост сам по себе - не больше пяти строк кода. 29 | Перейдем к рассмотрению реализации каждого из компонентов. 30 | 31 | #### `View` 32 | 33 | ```objc 34 | @interface PostContentCell : UITableViewCell 35 | 36 | @property (weak, nonatomic) IBOutlet UIWebView *contentWebView; 37 | @property (strong, nonatomic) id output; 38 | @property (strong, nonatomic) id webPresentation; 39 | 40 | @end 41 | 42 | @implementation PostContentCell 43 | 44 | - (BOOL)shouldUpdateCellWithObject:(PostContentCellObject *)object { 45 | [self.output didTriggerModuleSetupEventWithHtmlContent:object.htmlContent]; 46 | 47 | return YES; 48 | } 49 | 50 | #pragma mark - PostContentViewInput 51 | 52 | - (void)setupInitialState { 53 | [self.webPresentation setupWithWebView:self.contentWebView]; 54 | } 55 | 56 | #pragma mark - WebPresentationDelegate 57 | 58 | - (void)webPresentation:(id)webPresentation 59 | didTapLink:(NSURL *)link { 60 | [self.output didTriggerLinkTapEventWithURL:link]; 61 | } 62 | 63 | - (void)webPresentation:(id)webPresentation 64 | didTapImageWithLink:(NSURL *)link { 65 | [self.output didTriggerImageTapWithURL:link]; 66 | } 67 | 68 | @end 69 | ``` 70 | 71 | #### `PostContentPresenter` 72 | 73 | ```objc 74 | @implementation PostContentPresenter 75 | 76 | #pragma mark - PostContentViewOutput 77 | 78 | - (void)didTriggerModuleSetupEventWithHtmlContent:(NSString *)htmlContent { 79 | self.rawHtmlContent = htmlContent; 80 | 81 | [self.interactor renderContent:self.rawHtmlContent]; 82 | } 83 | 84 | - (void)didTriggerLinkTapEventWithURL:(NSURL *)url { 85 | [self.router showBrowserModuleWithURL:url]; 86 | } 87 | 88 | - (void)didTriggerImageTapWithURL:(NSURL *)url { 89 | [self.router showFullscreenImageModuleWithImageURL:url]; 90 | } 91 | 92 | #pragma mark - PostContentInteractorOutput 93 | 94 | - (void)didCompleteRenderingContent { 95 | CGFloat contentHeight = [self.view obtainCurrentContentHeight]; 96 | [self.moduleOutput didUpdatePostContentWithContentHeight:contentHeight]; 97 | } 98 | 99 | @end 100 | ``` 101 | 102 | #### `PostContentInteractor` 103 | 104 | ```objc 105 | @interface PostContentInteractor : NSObject 106 | 107 | @property (nonatomic, weak) id output; 108 | 109 | @property (nonatomic, strong) id webEngine; 110 | @property (nonatomic, strong) ContentHTMLComposer *contentComposer; 111 | @property (nonatomic, strong) JSScriptProvider *scriptProvider; 112 | @property (nonatomic, strong) ContentRenderStrategyFactory *renderStrategyFactory; 113 | 114 | @end 115 | 116 | @implementation PostContentInteractor 117 | 118 | #pragma mark - PostContentInteractorInput 119 | 120 | - (void)renderContent:(NSString *)content { 121 | NSArray *scripts = [self.scriptProvider obtainScriptsForPostEnvironmentSetup]; 122 | for (NSString *script in scripts) { 123 | [self.webEngine executeScript:script]; 124 | } 125 | 126 | NSString *processedContent = [self.contentComposer composeValidHtmlForRenderingFromRawHtml:content]; 127 | 128 | [self.webEngine loadHtml:processedContent]; 129 | } 130 | 131 | #pragma mark - WebEngineDelegate 132 | 133 | - (void)didCompleteLoadingContentWithWebEngine:(id)webEngine { 134 | ContentRenderStrategy *renderStrategy = [self.renderStrategyFactory postContentRenderStrategy]; 135 | [self.webEngine renderContentWithStrategy:renderStrategy]; 136 | } 137 | 138 | - (void)didCompleteRenderingContentWithWebEngine:(id)webEngine { 139 | [self.output didCompleteRenderingContent]; 140 | } 141 | 142 | @end 143 | ``` 144 | 145 | #### `WebEngineImplementation` 146 | 147 | ```objc 148 | @interface WebEngineImplementation : NSObject 149 | 150 | @property (weak, nonatomic) id webEngineDelegate; 151 | @property (weak, nonatomic) id webPresentationDelegate; 152 | @property (strong, nonatomic) WebBridgeFactory *bridgeFactory; 153 | 154 | @end 155 | 156 | @implementation WebEngineImplementation 157 | 158 | #pragma mark - WebPresentation 159 | 160 | - (void)setupWithWebView:(UIWebView *)webView { 161 | self.webView = webView; 162 | 163 | self.bridge = [self.bridgeFactory obtainBridgeForWebView:webView 164 | webViewDelegate:self]; 165 | 166 | [self setupJavascriptHandlers]; 167 | } 168 | 169 | #pragma mark - Handlers Setup 170 | 171 | - (void)setupJavascriptHandlers { 172 | @weakify(self); 173 | [self.bridge registerHandler:kImageTapHandler 174 | handler:^(id data, WVJBResponseCallback responseCallback) { 175 | @strongify(self); 176 | NSString *imageURLString = data[kDataLinkKey]; 177 | NSURL *imageURL = [NSURL URLWithString:imageURLString]; 178 | [self.webPresentationDelegate webPresentation:self 179 | didTapImageWithLink:imageURL]; 180 | }]; 181 | } 182 | 183 | #pragma mark - WebEngine 184 | 185 | - (void)loadHtml:(NSString *)html { 186 | [self.webView loadHTMLString:html 187 | baseURL:nil]; 188 | } 189 | 190 | - (void)executeScript:(NSString *)script { 191 | [self.webView stringByEvaluatingJavaScriptFromString:script]; 192 | } 193 | 194 | - (void)renderContentWithStrategy:(ContentRenderStrategy *)renderStrategy { 195 | dispatch_group_t group = dispatch_group_create(); 196 | JSResponseBlock responseBlock = ^(NSDictionary *responseData) { 197 | NSString *logDescription = responseData[JSLogDescriptionKey]; 198 | DDLogVerbose(@"%@", logDescription); 199 | dispatch_group_leave(group); 200 | }; 201 | 202 | for (NSString *handlerKey in renderStrategy.scriptNames) { 203 | dispatch_group_enter(group); 204 | [self.bridge callHandler:handlerKey 205 | data:renderStrategy.scriptPayloads[handlerKey] 206 | responseCallback:responseBlock]; 207 | } 208 | 209 | [self.webEngineDelegate didCompleteRenderingContentWithWebEngine:self]; 210 | } 211 | 212 | #pragma mark - UIWebViewDelegate 213 | 214 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 215 | [self.webEngineDelegate didCompleteLoadingContentWithWebEngine:self]; 216 | } 217 | 218 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 219 | NSString *urlString = request.URL.absoluteString; 220 | 221 | if (navigationType == UIWebViewNavigationTypeLinkClicked) { 222 | [self.webPresentationDelegate webPresentation:self 223 | didTapLink:request.URL]; 224 | return NO; 225 | } 226 | 227 | return YES; 228 | } 229 | ``` 230 | 231 | #### Заключение 232 | Помимо явного разделения ответственностей мы скрыли факт использования сторонней зависимости для связи нативного кода и JavaScript - это всего лишь детали реализации протокола ``. Unit-тесты для всех компонентов получились очень простыми. Все, что нужно проверить - это правильность data flow. 233 | -------------------------------------------------------------------------------- /english/webview.md: -------------------------------------------------------------------------------- 1 | ### UIWebView in VIPER 2 | 3 | The fundamental idea is to separate `UIWebView` responsibilities in two groups: `WebEngine`, which is a dependency of the Interactor, and `WebPresentation` which belongs to the View. 4 | 5 | #### WebEngine responsibilities 6 | 7 | - Loads HTML code. 8 | - Executes JavaScript. 9 | - Renders content with a set of predefined actions. 10 | - Notifies the Interactor of drawing, loading and rendering processes completion. 11 | 12 | #### WebPresentation responsibilities 13 | 14 | - Notifies the View of different events: links, images and different embeds taps. 15 | - Provides an interface of obtaining real content size and other view properties. 16 | 17 | ![Module Scheme](../Resources/webview-scheme.png) 18 | 19 | #### Module workflow 20 | 21 | 1. The Assembly setups two delegates for the `WebEngineImplementation` object - one is the View and another is the Interactor. 22 | 2. The Presenter (or the View in case of a cell) is an entry point of the module. It's only input is raw html data. 23 | 3. The Presenter receives View's `WebEngine` and passes it to the Interactor. 24 | 4. The Presenter passes html data to the Interactor. 25 | 5. The Interactor setups the `WebEngine` environment by executing a number of JavaScript scripts. 26 | 6. The Interactor passes html data to the `WebEngine`. 27 | 7. `WebEngine` notifies the Interactor on rendering completion. 28 | 8. The Interactor notifies the Presenter, which obtains the content size from the View and passes it to the module output. 29 | 30 | These are very simple steps - each method consists of no more than 5 lines of code. 31 | 32 | Let's investigate the implementation of each component. 33 | 34 | #### `View` 35 | 36 | ```objc 37 | @interface PostContentCell : UITableViewCell 38 | 39 | @property (weak, nonatomic) IBOutlet UIWebView *contentWebView; 40 | @property (strong, nonatomic) id output; 41 | @property (strong, nonatomic) id webPresentation; 42 | 43 | @end 44 | 45 | @implementation PostContentCell 46 | 47 | - (BOOL)shouldUpdateCellWithObject:(PostContentCellObject *)object { 48 | [self.output didTriggerModuleSetupEventWithHtmlContent:object.htmlContent]; 49 | 50 | return YES; 51 | } 52 | 53 | #pragma mark - PostContentViewInput 54 | 55 | - (void)setupInitialState { 56 | [self.webPresentation setupWithWebView:self.contentWebView]; 57 | } 58 | 59 | #pragma mark - WebPresentationDelegate 60 | 61 | - (void)webPresentation:(id)webPresentation 62 | didTapLink:(NSURL *)link { 63 | [self.output didTriggerLinkTapEventWithURL:link]; 64 | } 65 | 66 | - (void)webPresentation:(id)webPresentation 67 | didTapImageWithLink:(NSURL *)link { 68 | [self.output didTriggerImageTapWithURL:link]; 69 | } 70 | 71 | @end 72 | ``` 73 | 74 | #### `PostContentPresenter` 75 | 76 | ```objc 77 | @implementation PostContentPresenter 78 | 79 | #pragma mark - PostContentViewOutput 80 | 81 | - (void)didTriggerModuleSetupEventWithHtmlContent:(NSString *)htmlContent { 82 | self.rawHtmlContent = htmlContent; 83 | 84 | [self.interactor renderContent:self.rawHtmlContent]; 85 | } 86 | 87 | - (void)didTriggerLinkTapEventWithURL:(NSURL *)url { 88 | [self.router showBrowserModuleWithURL:url]; 89 | } 90 | 91 | - (void)didTriggerImageTapWithURL:(NSURL *)url { 92 | [self.router showFullscreenImageModuleWithImageURL:url]; 93 | } 94 | 95 | #pragma mark - PostContentInteractorOutput 96 | 97 | - (void)didCompleteRenderingContent { 98 | CGFloat contentHeight = [self.view obtainCurrentContentHeight]; 99 | [self.moduleOutput didUpdatePostContentWithContentHeight:contentHeight]; 100 | } 101 | 102 | @end 103 | ``` 104 | 105 | #### `PostContentInteractor` 106 | 107 | ```objc 108 | @interface PostContentInteractor : NSObject 109 | 110 | @property (nonatomic, weak) id output; 111 | 112 | @property (nonatomic, strong) id webEngine; 113 | @property (nonatomic, strong) ContentHTMLComposer *contentComposer; 114 | @property (nonatomic, strong) JSScriptProvider *scriptProvider; 115 | @property (nonatomic, strong) ContentRenderStrategyFactory *renderStrategyFactory; 116 | 117 | @end 118 | 119 | @implementation PostContentInteractor 120 | 121 | #pragma mark - PostContentInteractorInput 122 | 123 | - (void)renderContent:(NSString *)content { 124 | NSArray *scripts = [self.scriptProvider obtainScriptsForPostEnvironmentSetup]; 125 | for (NSString *script in scripts) { 126 | [self.webEngine executeScript:script]; 127 | } 128 | 129 | NSString *processedContent = [self.contentComposer composeValidHtmlForRenderingFromRawHtml:content]; 130 | 131 | [self.webEngine loadHtml:processedContent]; 132 | } 133 | 134 | #pragma mark - WebEngineDelegate 135 | 136 | - (void)didCompleteLoadingContentWithWebEngine:(id)webEngine { 137 | ContentRenderStrategy *renderStrategy = [self.renderStrategyFactory postContentRenderStrategy]; 138 | [self.webEngine renderContentWithStrategy:renderStrategy]; 139 | } 140 | 141 | - (void)didCompleteRenderingContentWithWebEngine:(id)webEngine { 142 | [self.output didCompleteRenderingContent]; 143 | } 144 | 145 | @end 146 | ``` 147 | 148 | #### `WebEngineImplementation` 149 | 150 | ```objc 151 | @interface WebEngineImplementation : NSObject 152 | 153 | @property (weak, nonatomic) id webEngineDelegate; 154 | @property (weak, nonatomic) id webPresentationDelegate; 155 | @property (strong, nonatomic) WebBridgeFactory *bridgeFactory; 156 | 157 | @end 158 | 159 | @implementation WebEngineImplementation 160 | 161 | #pragma mark - WebPresentation 162 | 163 | - (void)setupWithWebView:(UIWebView *)webView { 164 | self.webView = webView; 165 | 166 | self.bridge = [self.bridgeFactory obtainBridgeForWebView:webView 167 | webViewDelegate:self]; 168 | 169 | [self setupJavascriptHandlers]; 170 | } 171 | 172 | #pragma mark - Handlers Setup 173 | 174 | - (void)setupJavascriptHandlers { 175 | @weakify(self); 176 | [self.bridge registerHandler:kImageTapHandler 177 | handler:^(id data, WVJBResponseCallback responseCallback) { 178 | @strongify(self); 179 | NSString *imageURLString = data[kDataLinkKey]; 180 | NSURL *imageURL = [NSURL URLWithString:imageURLString]; 181 | [self.webPresentationDelegate webPresentation:self 182 | didTapImageWithLink:imageURL]; 183 | }]; 184 | } 185 | 186 | #pragma mark - WebEngine 187 | 188 | - (void)loadHtml:(NSString *)html { 189 | [self.webView loadHTMLString:html 190 | baseURL:nil]; 191 | } 192 | 193 | - (void)executeScript:(NSString *)script { 194 | [self.webView stringByEvaluatingJavaScriptFromString:script]; 195 | } 196 | 197 | - (void)renderContentWithStrategy:(ContentRenderStrategy *)renderStrategy { 198 | dispatch_group_t group = dispatch_group_create(); 199 | JSResponseBlock responseBlock = ^(NSDictionary *responseData) { 200 | NSString *logDescription = responseData[JSLogDescriptionKey]; 201 | DDLogVerbose(@"%@", logDescription); 202 | dispatch_group_leave(group); 203 | }; 204 | 205 | for (NSString *handlerKey in renderStrategy.scriptNames) { 206 | dispatch_group_enter(group); 207 | [self.bridge callHandler:handlerKey 208 | data:renderStrategy.scriptPayloads[handlerKey] 209 | responseCallback:responseBlock]; 210 | } 211 | 212 | [self.webEngineDelegate didCompleteRenderingContentWithWebEngine:self]; 213 | } 214 | 215 | #pragma mark - UIWebViewDelegate 216 | 217 | - (void)webViewDidFinishLoad:(UIWebView *)webView { 218 | [self.webEngineDelegate didCompleteLoadingContentWithWebEngine:self]; 219 | } 220 | 221 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 222 | NSString *urlString = request.URL.absoluteString; 223 | 224 | if (navigationType == UIWebViewNavigationTypeLinkClicked) { 225 | [self.webPresentationDelegate webPresentation:self 226 | didTapLink:request.URL]; 227 | return NO; 228 | } 229 | 230 | return YES; 231 | } 232 | ``` 233 | 234 | #### Summary 235 | 236 | Besides the clear separation of concerns, we've also hidden the fact of using a third party library for bridging - it's just an implementation detail of our `WebEngine`. Unit tests for this setup are easy and reliable - all that we have to test is the data flow. 237 | -------------------------------------------------------------------------------- /russian/frc.md: -------------------------------------------------------------------------------- 1 | ## NSFetchedResultsController в VIPER 2 | 3 | Одной из наиболее удобных возможностей, которые нам предоставляет использование **CoreData** для работы с графом объектов, является `NSFetchedResultsController`. В рамках архитектуры MVC его использование достаточно очевидно - контроллер экрана реализует протокол `NSFetchedResultsControllerDelegate`: 4 | 5 | ```objc 6 | - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath; 7 | - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller; 8 | - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller; 9 | ``` 10 | 11 | Эти методы действительно чрезвычайно удобны для того, чтобы прямо в них обновить стейт и вставить/перезагрузить/удалить некоторые из ячеек. За такую простоту, конечно, приходится платить: 12 | 13 | - Еще одна ответственность у контроллера, 14 | - Знание о CoreData выходит за пределы модельного/сервисного слоя, 15 | - Мы жестко привязываемся к выбранному механизму уведомлений об изменениях состояния базы, 16 | - *+100 строк* в и без того крупном классе. 17 | 18 | Часть из поставленных проблем в некоторой степени решаются путем декомпозиции контроллера на составные объекты, в том числе и на элементы VIPER-стека. 19 | 20 | В VIPER, в отличие от MVC, роль и место для `NSFetchedResultsController` не так четко определены. Однозначно не *View* - во-первых, она не может получать бизнес-сущности от кого-то из своего же слоя, во-вторых - в большинстве случаев стоит стараться вообще не использовать `NSManagedObject`'ы в чистом виде на верхнем уровне архитектуры. 21 | 22 | Не подходит и *Presenter* - его ответственность - связывать между собой элементы модуля и держать стейт. *FRC* же - это отдельный источник данных, не связанный с интерактором. 23 | 24 | Размещать на сервисном слое - тоже неправильно, поскольку сервисы - объекты пассивные, умеющие только реагировать на команды от верхнеуровневых компонентов, и не содержащие в себе никакого дополнительного стейта. 25 | 26 | 27 | Оптимальный вариант для размещения ответственности *FRC* - а под ней мы понимаем слежение за состоянием графа объектов, это интерактор: 28 | 29 | - Он уже работает с другими бизнес-сущностями, 30 | - В большинстве случаев он знает о том, что мы используем CoreData, 31 | - Он является источником данных текущего модуля - не важно, что послужило причиной их появления - прямой запрос из презентера или получение уведомления от базы. 32 | 33 | Мы пришли к двум различным вариантам работы с FRC на уровне интерактора. Первый подходит для таблиц с ограниченным количеством контента. Второй - для infinite scroll'а. 34 | 35 | #### Работа с таблицами с ограниченным количеством контента 36 | 37 | ##### Протокол CacheTracker 38 | 39 | `CacheTracker` - протокол объекта, задачей которого является получение уведомлений об изменении состояния базы и формировании на их основе набора транзакций. 40 | 41 | ```objc 42 | @protocol CacheTracker 43 | 44 | /** 45 | Метод настраивает трекер кеша 46 | 47 | @param cacheRequest Запрос, описывающий поведение трекера 48 | */ 49 | - (void)setupWithCacheRequest:(CacheRequest *)cacheRequest; 50 | 51 | /** 52 | Метод формирует батч транзакций исходя из текущего состояния кеша 53 | 54 | @return CacheTransactionBatch 55 | */ 56 | - (CacheTransactionBatch *)obtainTransactionBatchFromCurrentCache; 57 | 58 | @end 59 | ``` 60 | 61 | ##### Пример реализации `CacheTracker` 62 | 63 | Приведенный вариант реализации построен как раз на работе с `NSFetchedResultsController`. Альтернативная имплементация протокола могла бы, к примеру, быть построенной на работе с `NSNotification`'ами, получаемыми от CoreData. 64 | 65 | ```objc 66 | #pragma mark - Публичные методы 67 | 68 | - (void)setupWithCacheRequest:(CacheRequest *)cacheRequest { 69 | NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext]; 70 | NSFetchRequest *fetchRequest = [self fetchRequestWithCacheRequest:cacheRequest]; 71 | self.controller = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 72 | managedObjectContext:defaultContext 73 | sectionNameKeyPath:nil 74 | cacheName:nil]; 75 | self.controller.delegate = self; 76 | [self.controller performFetch:nil]; 77 | } 78 | 79 | - (NSFetchRequest *)fetchRequestWithCacheRequest:(CacheRequest *)cacheRequest { 80 | NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[cacheRequest.objectClass entityName]]; 81 | [fetchRequest setPredicate:cacheRequest.predicate]; 82 | [fetchRequest setSortDescriptors:cacheRequest.sortDescriptors]; 83 | return fetchRequest; 84 | } 85 | 86 | - (CacheTransactionBatch *)obtainTransactionBatchFromCurrentCache { 87 | CacheTransactionBatch *batch = [CacheTransactionBatch new]; 88 | for (NSUInteger i = 0; i < self.controller.fetchedObjects.count; i++) { 89 | id object = self.controller.fetchedObjects[i]; 90 | NSIndexPath *indexPath = [self.controller indexPathForObject:object]; 91 | id plainObject = [self.objectsFactory plainNSObjectForObject:object]; 92 | CacheTransaction *transaction = [CacheTransaction transactionWithObject:plainObject 93 | oldIndexPath:nil 94 | updatedIndexPath:indexPath 95 | objectType:NSStringFromClass(self.cacheRequest.objectClass) 96 | changeType:NSFetchedResultsChangeInsert]; 97 | [batch addTransaction:transaction]; 98 | } 99 | 100 | return batch; 101 | } 102 | 103 | #pragma mark - Методы NSFetchedResultsControllerDelegate 104 | 105 | - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { 106 | self.transactionBatch = [CacheTransactionBatch new]; 107 | } 108 | 109 | - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(NSManagedObject *)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { 110 | id plainObject = [self.objectsFactory plainNSObjectForObject:anObject]; 111 | CacheTransaction *transaction = [CacheTransaction transactionWithObject:plainObject 112 | oldIndexPath:indexPath 113 | updatedIndexPath:newIndexPath 114 | objectType:NSStringFromClass(self.cacheRequest.objectClass) 115 | changeType:changeType]; 116 | [self.transactionBatch addTransaction:transaction]; 117 | } 118 | 119 | - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { 120 | if ([self.transactionBatch isEmpty]) { 121 | return; 122 | } 123 | 124 | [self.delegate didProcessTransactionBatch:self.transactionBatch]; 125 | } 126 | ``` 127 | 128 | ##### CacheRequest 129 | 130 | `CacheRequest` - объект, содержащий в себе полное описание параметров слежения за состоянием базы, необходимых для `CacheTracker`. По сути, это запрос, на основе которого `CacheTracker` формирует свое поведение. 131 | 132 | ```objc 133 | @interface CacheRequest : NSObject 134 | 135 | @property (strong, nonatomic, readonly) NSPredicate *predicate; 136 | @property (strong, nonatomic, readonly) NSArray *sortDescriptors; 137 | @property (assign, nonatomic, readonly) Class objectClass; 138 | @property (strong, nonatomic, readonly) NSString *filterValue; 139 | 140 | + (instancetype)requestWithPredicate:(NSPredicate *)predicate 141 | sortDescriptors:(NSArray *)sortDescriptors 142 | objectClass:(Class)objectClass 143 | filterValue:(NSString *)filterValue; 144 | 145 | @end 146 | ``` 147 | 148 | ##### Классы транзакций 149 | 150 | `CacheTransaction` - объект, описывающий изменение одного `NSManagedObject`. 151 | 152 | ```objc 153 | @interface CacheTransaction : NSObject 154 | 155 | /** 156 | Измененный объект 157 | */ 158 | @property (strong, nonatomic, readonly) id object; 159 | 160 | /** 161 | IndexPath объекта до его изменения 162 | */ 163 | @property (strong, nonatomic, readonly) NSIndexPath *oldIndexPath; 164 | 165 | /** 166 | IndexPath объекта после его изменения 167 | */ 168 | @property (strong, nonatomic, readonly) NSIndexPath *updatedIndexPath; 169 | 170 | /** 171 | Тип измененного объекта 172 | */ 173 | @property (strong, nonatomic, readonly) NSString *objectType; 174 | 175 | /** 176 | Тип изменения 177 | */ 178 | @property (assign, nonatomic, readonly) NSFetchedResultsChangeType changeType; 179 | 180 | + (instancetype)transactionWithObject:(id)object 181 | oldIndexPath:(NSIndexPath *)oldIndexPath 182 | updatedIndexPath:(NSIndexPath *)updatedIndexPath 183 | objectType:(NSString *)objectType 184 | changeType:(NSUInteger)changeType; 185 | 186 | @end 187 | 188 | ``` 189 | 190 | `CacheTransactionBatch` объединяет набор транзакций в один объект, предназначенный для передачи между слоями прямо к таблице. 191 | 192 | ```objc 193 | @interface CacheTransactionBatch : NSObject 194 | 195 | @property (strong, nonatomic, readonly) NSOrderedSet *insertTransactions; 196 | @property (strong, nonatomic, readonly) NSOrderedSet *updateTransactions; 197 | @property (strong, nonatomic, readonly) NSOrderedSet *deleteTransactions; 198 | @property (strong, nonatomic, readonly) NSOrderedSet *moveTransactions; 199 | 200 | /** 201 | Метод добавляет в батч новую транзакцию 202 | 203 | @param transaction Транзакция 204 | */ 205 | - (void)addTransaction:(CacheTransaction *)transaction; 206 | 207 | /** 208 | Метод сообщает, содержит ли батч хоть одну транзакцию 209 | 210 | @return YES/NO 211 | */ 212 | - (BOOL)isEmpty; 213 | 214 | @end 215 | 216 | ``` 217 | 218 | ##### Пример интерактора 219 | 220 | Интерактор не делает практически никакой работы - ему нужно только правильно преднастроить `CacheTracker` и стать его делегатом. 221 | 222 | ```objc 223 | @implementation PostListInteractor 224 | 225 | - (void)setupCacheTrackingWithCacheRequest:(CacheRequest *)cacheRequest { 226 | [self.cacheTracker setupWithCacheRequest:cacheRequest]; 227 | CacheTransactionBatch *initialBatch = [self.cacheTracker obtainTransactionBatchFromCurrentCache]; 228 | [self.output didProcessCacheTransaction:initialBatch]; 229 | } 230 | 231 | #pragma mark - Методы протокола CacheTrackerDelegate 232 | 233 | - (void)didProcessTransactionBatch:(CacheTransactionBatch *)transactionBatch { 234 | [self.output didProcessCacheTransaction:transactionBatch]; 235 | } 236 | 237 | @end 238 | 239 | ``` 240 | 241 | ##### Работа с таблицей 242 | 243 | И финальная часть схемы - обработка таблицей полученного батча транзакций. В текущем варианте мы работаем с `UITableView` не напрямую, а с помощью фреймворка `Nimbus` - но сути действий это не меняет. 244 | 245 | ```objc 246 | - (void)updateDataSourceWithTransactionBatch:(CacheTransactionBatch *)transactionBatch { 247 | for (CacheTransaction *transaction in transactionBatch.insertTransactions) { 248 | PostListCellObject *cellObject = [self generateCellObjectForPost:transaction.object]; 249 | 250 | NSUInteger numberOfObjects = [self.tableViewModel lj_numberOfObjectsInSection:PostListSectionIndex]; 251 | NSUInteger updatedRow = transaction.updatedIndexPath.row; 252 | [self.tableViewModel insertObject:cellObject 253 | updatedRow 254 | inSection:PostListSectionIndex]; 255 | } 256 | 257 | for (CacheTransaction *transaction in transactionBatch.updateTransactions) { 258 | PostListCellObject *cellObject = [self generateCellObjectForPost:transaction.object]; 259 | NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:transaction.oldIndexPath.row 260 | inSection:PostListSectionIndex]; 261 | [self.tableViewModel removeObjectAtIndexPath:oldIndexPath]; 262 | [self.tableViewModel insertObject:cellObject 263 | atRow:transaction.updatedIndexPath.row 264 | inSection:PostListSectionIndex]; 265 | } 266 | 267 | NSMutableArray *removeIndexPaths = [NSMutableArray array]; 268 | for (CacheTransaction *transaction in transactionBatch.deleteTransactions) { 269 | NSIndexPath *removeIndexPath = [NSIndexPath indexPathForRow:transaction.oldIndexPath.row 270 | inSection:PostListSectionIndex]; 271 | [removeIndexPaths addObject:removeIndexPath]; 272 | } 273 | [self.tableViewModel lj_removeObjectsAtIndexPaths:[removeIndexPaths copy]]; 274 | 275 | for (CacheTransaction *transaction in transactionBatch.moveTransactions) { 276 | PostListCellObject *cellObject = [self generateCellObjectForPost:transaction.object]; 277 | NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:transaction.oldIndexPath.row 278 | inSection:PostListSectionIndex]; 279 | [self.tableViewModel removeObjectAtIndexPath:oldIndexPath]; 280 | [self.tableViewModel insertObject:cellObject 281 | atRow:transaction.updatedIndexPath.row 282 | inSection:PostListSectionIndex]; 283 | } 284 | } 285 | ``` 286 | 287 | #### Работа с infinite scroll 288 | 289 | При работе с бесконечными лентами держать в памяти все полученные объекты может быть очень накладно. В таком случае лучше использовать модифицированный первый вариант. Изменения следующие: 290 | 291 | - `CacheTracker` не добавляет в транзакции модельные объекты, а передает только индексы. 292 | - Таблица умеет запрашивать объект по индексу через цепочку `Presenter -> Interactor -> CacheTracker`. 293 | -------------------------------------------------------------------------------- /russian/code-style.md: -------------------------------------------------------------------------------- 1 | Использование всеми разработчиками команды одного code style не менее важно, чем единая точка зрения на архитектуру приложения и ответственности разных объектов. Наличие соглашений по работе с VIPER-стеком не исключение. 2 | 3 | ### Общие правила для модуля 4 | 5 | - Название модуля должно полностью отражать его назначение. Суффикс *Module* в название включать не следует. 6 | 7 | **Пример:** `MessageFolder`, `PostList`, `CacheSettings`. 8 | 9 | - Все элементы модуля разбиты по подпапкам в рамках одной папки модуля. 10 | 11 | **Пример:** 12 | 13 | ``` 14 | /NewPostUserStory 15 | /NewPostModule 16 | /Assembly 17 | /Interactor 18 | /Presenter 19 | /Router 20 | /View 21 | /ChooseAvatarModule 22 | /Assembly 23 | /Interactor 24 | /Presenter 25 | /Router 26 | /View 27 | ``` 28 | 29 | - Если по итогу написания модуля какие-то из его элементов остались неиспользованными, будь то классы, протоколы или методы, они удаляются. 30 | - Все хелперы располагаются в подпапке своего слоя. 31 | 32 | **Пример:** 33 | 34 | ``` 35 | /Interactor 36 | /UserInputValidator 37 | UserInputValidator.h 38 | UserInputValidator.m 39 | /PlainObjectMapper 40 | PlainObjectMapper.h 41 | PlainObjectMapperImplementation.h 42 | PlainObjectMapperImplementation.m 43 | ``` 44 | - Все методы, с помощью которых слои общаются друг с другом, должны быть синхронными. 45 | 46 | **Пример:** 47 | 48 | ```objc 49 | @interface InteractorInput 50 | - (void)obtainDataFromNetwork; 51 | ... 52 | 53 | @interface InteractorOutput 54 | - (void)didObtainDataFromNetwork:(NSArray *)data; 55 | ... 56 | ``` 57 | 58 | - Все методы протоколов, которыми закрыты элементы модуля, должны начинаться с глаголов - это помогает явно указать на то, что каждый из компонентов обладает поведением, а не состоянием. 59 | - Методы, обозначающие начало действия, должны начинаться с глаголов, выражающих приказ или просьбу (глаголов повелительного наклонения). 60 | - Методы, обозначающие завершение действия или процесса, констатацию факта должны начинаться с глагола прошедшего времени. 61 | 62 | **Пример:** 63 | 64 | ```objc 65 | - (void)obtainImageForPostId:(NSString *)postId; 66 | - (void)processUserInput:(NSString *)userInput; 67 | - (void)invalidateCurrentCache; 68 | ``` 69 | 70 | - В публичных интерфейсах всех классов и протоколов стараемся использовать forward-declaration, используя `#import` лишь при необходимости. 71 | 72 | **Пример:** 73 | 74 | ```objc 75 | #import 76 | 77 | #import "PostListViewOutput.h" 78 | #import "PostListModuleInput.h" 79 | #import "PostListInteractorOutput.h" 80 | 81 | @protocol PostListViewInput; 82 | @protocol PostListRouterInput; 83 | @protocol PostListInteractorInput; 84 | @class PostListViewModelMapper; 85 | 86 | @interface PostListPresenter : NSObject 87 | 88 | @property (nonatomic, weak) id view; 89 | @property (nonatomic, strong) id router; 90 | @property (nonatomic, strong) id interactor; 91 | @property (nonatomic, strong) PostListViewModelMapper *postListViewModelMapper; 92 | 93 | @end 94 | ``` 95 | 96 | ----- 97 | 98 | ### Слой Interactor 99 | #### Класс `Interactor` 100 | 101 | ##### Наименование 102 | `Interactor.h / Interactor.m` 103 | 104 | ##### Дополнительные правила 105 | 106 | - Интерактор не держит состояния, только зависимости, расположенные в его открытом интерфейсе. 107 | 108 | **Пример:** 109 | 110 | ```objc 111 | @interface PostListInteractor : NSObject 112 | 113 | @property (nonatomic, weak) id output; 114 | @property (nonatomic, strong) id accountService; 115 | 116 | @end 117 | ``` 118 | 119 | - Интерактор держит weak-ссылку на презентер. Переменная называется `output`. 120 | 121 | **Пример:** 122 | 123 | `@property (nonatomic, weak) id output;` 124 | 125 | #### Фасады над сервисами 126 | ##### Наименование 127 | `Facade.h / Facade.m` 128 | 129 | ##### Описание 130 | В случае, если в интеракторах нескольких модулей есть повторяющаяся логика по использованию сервисов определенным образом, она инкапсулируется в отдельный фасад над сервисами. В качестве примера можно привести объект, реализующий логику пагинации разных лент постов - он умеет запрашивать список новых элементов, сранивать их с ранее закешированными, вычислять смещения и дырки - и многое другое. Благодаря выделению этих связей в отдельную сущность, пагинация может быть достаточно легко подключена для любого модуля списка элементов. 131 | 132 | **Пример:** `PagingFacade`. 133 | 134 | #### Протокол `` 135 | ##### Наименование 136 | `InteractorInput.h` 137 | 138 | ##### Описание 139 | Содержит методы для общения с интерактором. Этим протоколом закрыт интерактор с точки зрения презентера. 140 | 141 | ##### Примеры методов 142 | 143 | ```objc 144 | - (NSArray *)obtainNewsFromCache; 145 | - (void)obtainMessageWithId:(NSString *)messageId; 146 | - (void)performLoginWithUsername:(NSString *)username password:(NSString *)password; 147 | ``` 148 | 149 | ##### Общие паттерны методов 150 | 151 | - Если в данном модуле нам нужно самостоятельно решать, в какой момент просить данные из кэша, а в какой - из сети, допустимо явно указывать это интерактору (`obtainFromNetwork/-fromCache`). 152 | 153 | #### Протокол `` 154 | ##### Наименование 155 | `InteractorOutput.h` 156 | 157 | ##### Описание 158 | Содержит методы, при помощи которых интерактор общается с вышестоящим слоем модуля. Этим протоколом обычно закрыт презентер. 159 | 160 | ##### Примеры методов 161 | 162 | ```objc 163 | - (void)didObtainMessage:(Message *)message; 164 | - (void)didPerformLoginWithSuccess; 165 | ``` 166 | 167 | ##### Общие паттерны методов: 168 | 169 | - В большинстве случаев в качестве префикса каждого метода используем `did` - это указывает на пассивную роль интерактора, который умеет выполнять ряд действий по запросу и уведомлять об их окончании. 170 | - В случае отсутствия единой системы обработки ошибок заводятся пары методов (`didObtainWithSuccess/-withFailure`). 171 | 172 | ### Слой Presenter 173 | #### Класс `Presenter` 174 | ##### Наименование 175 | `Presenter.h / Presenter.m` 176 | 177 | ##### Дополнительные правила 178 | 179 | - В отличие от всех остальных элементов, презентер обладает состоянием. Оно находится в приватном extension. 180 | - Презентер держит weak-ссылку на view. Переменная называется `view`. 181 | 182 | **Пример:** 183 | 184 | `@property (nonatomic, weak) id view;` 185 | 186 | - Презентер держит strong-ссылку на роутер. Переменная называется `router`. 187 | 188 | **Пример:** 189 | 190 | `@property (nonatomic, strong) id router;` 191 | 192 | - Презентер держит strong-ссылку на интерактор. Переменная называется `interactor`. 193 | 194 | **Пример:** 195 | 196 | `@property (nonatomic, strong) id interactor;` 197 | 198 | - Если презентеру нужно держать объект, реализующий протокол `ModuleInput` дочернего модуля, переменная называется `ModuleInput`. 199 | 200 | #### Класс `State` 201 | ##### Наименование 202 | `State.h / State.m` 203 | 204 | ##### Описание 205 | В том случае, если текущий модуль обладает каким-либо состоянием, его можно выделить в отдельный объект, не обладающий никаким поведением и выступающий простым хранилищем данных. 206 | 207 | **Пример:** 208 | 209 | ```objc 210 | @interface PostListState 211 | 212 | @property (nonatomic, assign) FeedType feedType; 213 | @property (nonatomic, assign) BOOL hasHeader; 214 | @property (nonatomic, strong) NSString *feedId; 215 | 216 | @end 217 | ``` 218 | 219 | #### Протокол `` 220 | ##### Наименование 221 | `ModuleInput.h` 222 | 223 | ##### Описание 224 | Содержит методы, при помощи которых с модулем могут общаться другие модули или его контейнер. 225 | 226 | ##### Дополнительные правила 227 | - При использовании библиотеки ViperMcFlurry наследуется от протокола ``. 228 | 229 | ##### Примеры методов 230 | 231 | ```objc 232 | - (void)configureWithPostId:(NSString *)postId; 233 | - (void)updateContentInset:(CGFloat)contentInset; 234 | ``` 235 | 236 | #### Протокол `` 237 | ##### Наименование 238 | `ModuleOutput.h` 239 | 240 | ##### Описание 241 | Содержит методы, при помощи которых модуль общается со своим контейнером или другими модулями. 242 | 243 | ##### Дополнительные правила 244 | - При использовании библиотеки ViperMcFlurry наследуется от протокола ``. 245 | 246 | ##### Примеры методов 247 | 248 | ```objc 249 | - (void)didSelectMenuItem:(NSString *)menuItem; 250 | - (void)didPerformLoginWithSuccess; 251 | ``` 252 | 253 | ### Слой Router 254 | #### Класс `Router` 255 | ##### Наименование 256 | `Router.h / Router.m` 257 | 258 | ##### Дополнительные правила 259 | - При использовании библиотеки ViperMcFlurry держит weak-ссылку на `ViewController`, отвечающий за переходы этого модуля. Ссылка представляет собой свойство, закрытое протоколом ``. Обычно эта переменная называется `transitionHandler` 260 | 261 | #### Класс `Route` 262 | ##### Наименование 263 | `Route.h / Route.m` 264 | 265 | ##### Описание 266 | Если несколько роутеров в рамках одного приложения реализуют повторяющуюся логику по переходам на один экран - ее можно инкапсулировать в отдельном объекте-маршруте, который будет подключаться к нужным модулям. К примеру, это может пригодиться в случае экрана авторизации, на который можно перейти из разных модулей - настроек, профиля, бокового меню. Благодаря инкапсуляции этой логики в отдельном объекте, мы избавляемся от необходимости в роутере каждого из этих модулей писать один и тот же код. 267 | 268 | ##### Примеры методов 269 | 270 | ```objc 271 | - (void)openAuthorizationModuleWithTransitionHandler:(id)transitionHandler; 272 | ``` 273 | 274 | #### Протокол `` 275 | ##### Наименование 276 | `RouterInput.h` 277 | 278 | ##### Описание 279 | Содержит методы переходов на другие модули, которые могут быть вызваны презентером. 280 | 281 | ##### Примеры методов 282 | 283 | ```objc 284 | - (void)openDetailNewsModuleWithNewsId:(NSString *)newsId 285 | - (void)closeCurrentModule; 286 | ``` 287 | 288 | ##### Общие паттерны методов 289 | 290 | - Для консистентности все методы этого протокола начинаются либо на `open-` (открытие какого-либо модуля), `close-` (закрытие модуля) или `embed-` (встраивание дочернего модуля в контейнер). 291 | 292 | ### Слой View 293 | #### Классы отображения (ViewController, View, Cell) 294 | ##### Наименование 295 | `View.h / View.m`, `ViewController.h / ViewController.m`, `Cell.h / Cell.m`. 296 | 297 | ##### Дополнительные правила 298 | 299 | - Все `IBOutlet`'ы и `IBAction`'ы (то есть все зависимости и интерфейс) View выносятся в его публичный интерфейс. 300 | - View держит strong-ссылку на презентер. Переменная называется `output`. 301 | 302 | **Пример:** 303 | 304 | `@property (nonatomic, strong) id output;` 305 | 306 | #### Класс DataDisplayManager 307 | ##### Наименование 308 | `DataDisplayManager.h / DataDisplayManager.m` 309 | 310 | ##### Описание 311 | Объект, закрывающий логику реализации `UITableViewDataSource` и `UITableViewDelegate`. Работает только с данными, ничего не знает о конкретных `UIView` экрана. Обычно протоколом не закрывается, потому что конкретному экрану чаще всего соответствует одна конкретная реализация DataDisplayManager. 312 | 313 | ##### Примеры методов 314 | 315 | ```objc 316 | - (id)dataSourceForTableView:(UITableView *)tableView; 317 | - (id)delegateForTableView:(UITableView *)tableView 318 | withBaseDelegate:(id )baseTableViewDelegate; 319 | ``` 320 | 321 | #### Класс CellObjectFactory 322 | ##### Наименование 323 | `CellObjectFactory.h / CellObjectFactory.m` 324 | 325 | ##### Описание 326 | Зачастую удобно бывает выносить логику по созданию моделей ячеек из DataDisplayManager'а в отдельный объект, который, по сути, преобразует обычные модели в CellObject'ы. 327 | 328 | #### Протокол 329 | ##### Наименование 330 | `ViewInput.h` 331 | 332 | ##### Описание 333 | Содержит методы, при помощи которых презентер может управлять отображением или получать введенные пользователем данные. 334 | 335 | ##### Примеры методов 336 | 337 | ```objc 338 | - (void)updateWithTitle:(NSString *)title; 339 | - (NSString *)obtainCurrentUserInput; 340 | ``` 341 | 342 | #### Протокол 343 | ##### Наименование 344 | `ViewOutput.h` 345 | 346 | ##### Описание 347 | Содержит методы, при помощи которых View уведомляет презентер об изменениях своего состояния. 348 | 349 | ##### Дополнительные правила 350 | - В этом же протоколе находятся и методы, при помощи которых View уведомляет презентер о событиях своего жизненного цикла. 351 | 352 | ##### Примеры методов 353 | 354 | ```objc 355 | - (void)didTapLoginButton; 356 | - (void)didModifyCurrentInput; 357 | ``` 358 | 359 | ##### Общие паттерны методов 360 | - В большинстве случаев в качестве префикса каждого метода используем `did` - это указывает на пассивную роль View, который умеет выполнять ряд действий по запросу и уведомлять об их окончании. 361 | 362 | **Примеры:** 363 | 364 | ```objc 365 | - (void)didTriggerViewWillAppearEvent; 366 | - (void)didTriggerMemoryWarningEvent; 367 | ``` 368 | 369 | ### Слой Assembly 370 | #### Класс Assembly 371 | ##### Наименование 372 | `Assembly.h / Assembly.m` 373 | 374 | ##### Дополнительные правила 375 | 376 | - В интерфейс Assembly выносится только метод, конфигурирующий View. Установка всего VIPER-стека - это детали реализации assembly, которые не должны быть известны окружающему миру. 377 | 378 | ##### Примеры методов 379 | 380 | ```objc 381 | 382 | - (PostListViewController *)viewPostListModule; 383 | - (PostListPresenter *)presenterPostListModule; 384 | 385 | ``` 386 | 387 | ##### Общие паттерны методов 388 | 389 | - Для более удобной автоподстановки у всех методов, создающих стандартные компоненты, указывается префикс `Module`. 390 | 391 | ----- 392 | 393 | ### Комментарии 394 | 395 | - Все методы протоколов и конкретных классов обязательно покрываются подробными javadoc-комментариями. 396 | 397 | 398 | **Пример:** 399 | 400 | ```objc 401 | @protocol PostListInteractorInput 402 | 403 | /** 404 | Метод возвращает модель поста по определенному индексу 405 | 406 | @param index Индекс поста в рамках просматриваемой категории 407 | 408 | @return Пост 409 | */ 410 | - (PostModelObject *)obtainPostAtIndex:(NSUInteger)index; 411 | 412 | /** 413 | Метод возвращает общее количество постов для текущей ленты 414 | 415 | @return Количество постов 416 | */ 417 | - (NSUInteger)obtainOverallPostCount; 418 | 419 | @end 420 | ``` 421 | 422 | - Комментарии пишутся к интерфейсам всех уникальных компонентов модуля (различные хелперы, ячейки). 423 | 424 | **Пример:** 425 | 426 | ```objc 427 | /** 428 | Ячейка, используемая для краткого отображения поста в списке 429 | */ 430 | @interface PostListCell : UITableViewCell 431 | 432 | ... 433 | 434 | @end 435 | ``` 436 | 437 | - К интерфейсам всех неуникальных компонентов (интерактор, презентер, view), а также к их протоколам, пишется одинаковый комментарий, описывающий предназначение всего модуля. 438 | 439 | **Пример:** 440 | 441 | ```objc 442 | /** 443 | Модуль отвечает за отображение списка постов в любой из лент приложения. 444 | Используется как embedded-модуль. 445 | */ 446 | @interface PostListAssembly : TyphoonAssembly 447 | ... 448 | @end 449 | ``` 450 | 451 | - В имплементации классов группы методов различных протоколов разбиваются при помощи `#pragma mark -`. 452 | 453 | **Пример:** 454 | 455 | ```objc 456 | @implementation PostListPresenter 457 | 458 | #pragma mark - PostListModuleInput 459 | 460 | - (void)configureModuleWithPostListConfig:(PostListConfig *)config { 461 | ... 462 | } 463 | 464 | #pragma mark - PostListViewOutput 465 | 466 | - (void)didTriggerPullToRefreshEvent { 467 | ... 468 | } 469 | 470 | #pragma mark - PostListInteractorOutput 471 | 472 | - (void)didProcessCacheTransaction:(CacheTransactionBatch *)transaction { 473 | ... 474 | } 475 | 476 | ``` 477 | 478 | ----- 479 | 480 | ### Тесты 481 | 482 | - Для каждого компонента модуля создается отдельный тест-кейс с названием вида `ViewControllerTests.m`. 483 | - Стараемся придерживаться правила один тест - одна проверка. 484 | - Для разделения реализации теста на логические блоки используем нотацию *given/when/then*. 485 | - Тесты методов различных протоколов разбиваются при помощи `#pragma mark -`. 486 | - Так как в интерфейсе Assembly объявлены не все методы, для их проверки создается отдельный extension `_Testable.h`. В этом случае мы тестируем приватные методы, но такой подход обусловлен деталями реализации библиотеки *Typhoon*. В случае написания фабрики вручную, возможно проверить результаты работы компонента через его публичный интерфейс. 487 | - Файловая структура модуля в тестах максимально повторяет файловую структуру проекта. 488 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | ### Using Creative Commons Public Licenses 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 10 | 11 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 12 | 13 | ## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | ### Section 1 – Definitions. 18 | 19 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. __BY-NC-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. 24 | 25 | d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | 27 | e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 28 | 29 | f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 30 | 31 | g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. 32 | 33 | h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 34 | 35 | i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 36 | 37 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 38 | 39 | i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 40 | 41 | j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 42 | 43 | k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 44 | 45 | l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 46 | 47 | ### Section 2 – Scope. 48 | 49 | a. ___License grant.___ 50 | 51 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 52 | 53 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 54 | 55 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 56 | 57 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 58 | 59 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 60 | 61 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 62 | 63 | 5. __Downstream recipients.__ 64 | 65 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 66 | 67 | B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. 68 | 69 | C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 70 | 71 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 72 | 73 | b. ___Other rights.___ 74 | 75 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 76 | 77 | 2. Patent and trademark rights are not licensed under this Public License. 78 | 79 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 80 | 81 | ### Section 3 – License Conditions. 82 | 83 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 84 | 85 | a. ___Attribution.___ 86 | 87 | 1. If You Share the Licensed Material (including in modified form), You must: 88 | 89 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 90 | 91 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 92 | 93 | ii. a copyright notice; 94 | 95 | iii. a notice that refers to this Public License; 96 | 97 | iv. a notice that refers to the disclaimer of warranties; 98 | 99 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 100 | 101 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 102 | 103 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 104 | 105 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 106 | 107 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 108 | 109 | b. ___ShareAlike.___ 110 | 111 | In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 112 | 113 | 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. 114 | 115 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 116 | 117 | 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 118 | 119 | ### Section 4 – Sui Generis Database Rights. 120 | 121 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 122 | 123 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 124 | 125 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and 126 | 127 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 128 | 129 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 130 | 131 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 132 | 133 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 134 | 135 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 136 | 137 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 138 | 139 | ### Section 6 – Term and Termination. 140 | 141 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 142 | 143 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 144 | 145 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 146 | 147 | 2. upon express reinstatement by the Licensor. 148 | 149 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 150 | 151 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 152 | 153 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 154 | 155 | ### Section 7 – Other Terms and Conditions. 156 | 157 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 158 | 159 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 160 | 161 | ### Section 8 – Interpretation. 162 | 163 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 164 | 165 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 166 | 167 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 168 | 169 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 170 | 171 | ``` 172 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 173 | 174 | Creative Commons may be contacted at creativecommons.org 175 | ``` -------------------------------------------------------------------------------- /russian/mvc-chainsaw-massacre.md: -------------------------------------------------------------------------------- 1 | Начать новый проект, заложив в его основу VIPER-архитектуру, несложно. Но программисты в своей практике постоянно сталкиваются с задачей поддержки и развития приложений, чья кодовая база изначально разрабатывалась хаотично, без применения жестких правил проектирования. Часто бывает, что требований к первой версии проекта немного, они умещаются на одном тетрадном листе, и, соответственно, при разработке не уделяется должное внимание архитектуре приложения. А техническое задание ко второй версии получается не менее объемным, чем "Война и мир" Толстого, что, естественно, требует кардинально изменить подход к развитию проекта. Поэтому более сложной, но и более интересной задачей является миграция уже в какой-то степени готового iOS-приложения со слабым фундаментом на прочное основание гибкой и надежной архитектуры, которой является VIPER. 2 | 3 | ### Почему MVC становится Massive-View-Controller 4 | 5 | Базовый архитектурный шаблон iOS-приложений, предлагаемый Apple, - это **Model-View-Controller (MVC, "модель-представление-контроллер")**. Обычно этому паттерну явно или неявно следуют все начинающие разработчики. Шаблон Model-View-Controller, что очевидно, - трехслойный. При его использовании все объекты приложения в зависимости от своего назначения принадлежат одному из трех слоев: модели (Model), представлению (View) или управлению (Controller). Каждый архитектурный слой отделен от другого абстрактными границами, через которые осуществляется связь между объектами разных слоев. Основная цель применения этой архитектурной концепции состоит в отделении бизнес-логики (модели) от её визуализации (представления). 6 | 7 | **Модельный слой** описывает данные приложения и определяет логику их обработки и хранения. Объекты модели не должны иметь явной связи с объектами представления; они не содержат информации, как данные можно визуализировать. Примерами объектов этого слоя являются объекты хранения данных, парсеры, сетевые клиенты и т.д. 8 | 9 | **Слой представления** содержит объекты, которые пользователь может увидеть. Чаще всего, объекты этого слоя переиспользуемы. К ним относятся, например, **UILabel** и **UIButton**. 10 | 11 | **Слой управления** выступает посредником при взаимодействии объектов слоя представления и модельных объектов: контролирует ввод данных пользователем и использует модель и представление для реализации необходимой реакции. Также объекты этого слоя выполняют постановку и согласование задач приложения, управление жизненным циклом других объектов. 12 | 13 | ![Model-View-Controller](../Resources/MVCMassacre/MVC.png) 14 | 15 | > **Замечание**. Несмотря на то, что в своей документации Apple называет описываемый архитектурный шаблон классическим Model-View-Controller, его правильное название - **Model–View–Adapter (MVA, "модель-представление-адаптер")** или **mediating-controller Model-View-Controller**. 16 | > 17 | > MVA и MVC решают одну и ту же проблему, но используют разный подход. Классический MVC представляется в виде треугольника, где вершинами являются слои: модель, представление и управление, - и разрешает обмен данными между моделью и представлением в обход контроллера. MVA же располагает три слоя на одной линии, исключая прямой обмен данными между моделью и представлением (как на рисунке выше). 18 | > 19 | > В статье здесь и дальше будет использоваться название Model-View-Controller для исключения расхождений с официальной документацией компании Apple. 20 | 21 | Согласно рекомендаций Apple, ядром слоя управления в iOS-приложении выступают **контроллеры представления (view controllers)**. Каждый такой объект отвечает за 22 | 23 | * управление иерархией представлений; 24 | * адаптацию размеров представлений к определяемому устройством пространству отображения; 25 | * обновление содержимого представлений в ответ на изменение данных; 26 | * обработку пользовательского ввода и передачу полученных данных в модельный слой; 27 | * освобождение связанных ресурсов при нехватке доступной оперативной памяти. 28 | 29 | Рекомендуемым способом описания view controller и связанных с ним view является **Storyboard editor**. Используя этот инструмент, можно не только указать, какие объекты представления какому view controller принадлежат, но также определить иерархические связи и переходы между различными контроллерами. 30 | ![Storyboard editor](../Resources/MVCMassacre/Storyboard editor.png) 31 | Apple дает несколько советов по созданию контроллеров представления. 32 | 33 | * **Используйте поставляемые со стандартным SDK классы view controllers.** 34 | 35 | В iOS SDK имеется множество контроллеров представления, решающих конкретные задачи: от доступа к списку контактов пользователя до отображения медиаданных. Хорошей практикой является использование в своих приложениях таких, поставляемых системными библиотеками, контроллеров. 36 | 37 | * **Создавайте view controller максимально автономным.** 38 | 39 | Контроллер представления не должен знать о внутренней логике другого контроллера или о его иерархии view. Обмен данными между двумя контроллерами должен осуществляться через явно определенный публичный интерфейс. 40 | 41 | * **Не храните во view controller данные.** 42 | 43 | View controller выступает посредником между модельным слоем и слоем представления при обмене данными. Он может кешировать некоторые данные для быстрого доступа, валидировать их, но его основная обязанность - гарантировать, что view отображает правильную информацию. 44 | 45 | * **Используйте view controller для реакции на внешние события.** 46 | 47 | К внешним событиям относятся: пользовательский ввод, системные уведомления (например, о появлении клавиатуры), делегатные методы различных обработчиков (например, [CLLocationManager](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/index.html#//apple_ref/doc/uid/TP40007125-CH3)). 48 | 49 | Приведенные рекомендации и советы, несмотря на свою полезность, слишком общие, описаны без конкретизации и поэтому имеют ряд недостатков. Главный из них - неконтролируемый рост сложности контроллеров представления. Их неразрывная связь с жизненным циклом представлений, являющаяся особенностью iOS SDK, требует формировать реакции на события этого цикла непосредственно во view controller. Реализация обработки внешних событий, пользовательского ввода также происходит во view controller. Помимо этого, модель часто становится слишком пассивной, используется исключительно для доступа к данным, а вся бизнес-логика оказывается вновь во view controller. Отдельно стоит отметить, что Apple практически не рассматривает важные вопросы организации двунаправленной передачи данных между контроллерами, конфигурации созданных контроллеров и т.п., что приводит большинство разработчиков к выводу, что за них опять же ответственен view controller. 50 | 51 | В итоге, контроллеры представления становятся центром практически всего, что происходит в приложении и, как следствие, разрастаются до гигантских размеров, превращаясь в то, что называется **Massive-** или **Mega-View-Controller**. Подобный объект является отличным примером последствий необдуманной разработки, игнорирующей базовые принципы проектирования. 52 | 53 | ![Massive-View-Controller](../Resources/MVCMassacre/MassiveViewControllerScheme.png) 54 | 55 | Можно выделить следующие недостатки Massive-View-Controller. 56 | 57 | * **Высокая сложность поддержки и развития.** 58 | 59 | Код в Massive-View-Controller сложно модифицировать и расширять из-за его высокой связности и неочевидных потоков данных. Существует риск, что при внесении изменений можно сломать текущую функциональность приложения и до определенного момента не замечать этого. 60 | 61 | * **Высокий порог вхождения.** 62 | 63 | Найти в большом объеме кода нужный метод может оказаться непростой задачей: структура кода неявна и требует время на изучение. 64 | 65 | * **Код слабо тестируем.** 66 | 67 | Из-за высокой связности код Massive-View-Controller не покрывается модульными тестами. Так, попытка проверить логику представления приводит к явному вызову методов жизненного цикла, которые неявно могут повлечь за собой загрузку всех связанных view, что нарушает принципы unit-тестирования. 68 | 69 | * **Код практически невозможно переиспользовать.** 70 | 71 | Реализация новой функциональности приложения происходит непосредственно в контроллере представления. Соответственно, переиспользовать этот код без переиспользования всего контроллера не представляется возможным, а рефакторинг и декомпозиция объемного объекта может оказаться нетривиальной задачей. 72 | 73 | Чтобы избежать перечисленных недостатков и не загнать в тупик разработку приложения, необходимо строго следовать требованиям хорошего, продуманного архитектурного шаблона. Model-View-Controller, несмотря на слабые места и неочевидность решения некоторых задач проектирования, вследствие простоты использования подойдет для небольших приложений или приложений, требующих высокую скорость разработки. Но для крупных и сложных, с долговременным циклом поддержки и развития лучше использовать более гибкую архитектуру - VIPER. 74 | 75 | ### От плохой реализации к хорошей. Рефакторинг Massive-View-Controller 76 | 77 | Столкнувшись с задачей развития приложения, код которого отягчен Massive-View-Controller, прежде всего необходимо улучшить структуру проекта. Как следствие, требуется миграция большого и трудноподдерживаемого Massive-View-Controller во что-то более удобное и гибкое, например, VIPER-модуль. 78 | В VIPER, в отличие от MVC, контроллер представления выступает ядром view-слоя, что позволяет не нагружать его дополнительной логикой и локализовать в нем только реализацию задач, непосредственно связанных с визуализацией данных. В этом случае view controller, получая различные внешние сигналы (пользовательский ввод, системные уведомления о нехватке памяти и т.д.), передает их на обработку нижележащему слою - презентеру. Вся остальная невизуальная логика модуля, реализуемая в MVC непосредственно во view controller (например, создание других модулей или передача данных между ними), также инкапуслирована в других слоях. 79 | 80 | Ниже приведено три примера перехода от Massive-View-Controller к VIPER-модулю. В качестве исходной используется реализация контроллера представления из реального проекта - гида по одному из городов мира. В выбранном в качестве примера view controller реализовано главное навигационное меню. 81 | 82 | ![Storyboard editor](../Resources/MVCMassacre/MassiveViewControllerExample.png) 83 | 84 | #### Примеры рефакторинга 85 | 86 | * **Переход между модулями** 87 | 88 | В данном коде реализуется переход во внутренний раздел приложения. Направление перехода определяется по индексу выбранной ячейки таблицы меню. 89 | 90 | ```objective-c 91 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 92 | long index = indexPath.row; 93 | switch (index) { 94 | case MainMenuCellsCity: { 95 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Quarters" 96 | factory:AGAppDelegateMacros.storyboardFactory 97 | bundle:nil]; 98 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agquarterslistviewcontroller"]; 99 | [self.navigationController pushViewController:vc 100 | animated:YES]; 101 | } 102 | break; 103 | case MainMenuCellsPlaces: { 104 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Places" 105 | factory:AGAppDelegateMacros.storyboardFactory 106 | bundle:nil]; 107 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agplacescategorieslist"]; 108 | [self.navigationController pushViewController:vc 109 | animated:YES]; 110 | } 111 | break; 112 | case MainMenuCellsReferenceBook: { 113 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"ReferenceBook" 114 | factory:AGAppDelegateMacros.storyboardFactory 115 | bundle:nil]; 116 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agreferencebookcategoriesviewcontroller"]; 117 | [self.navigationController pushViewController:vc 118 | animated:YES]; 119 | } 120 | break; 121 | } 122 | } 123 | ``` 124 | 125 | Эта реализация имеет ряд проблем. 126 | 127 | * Слой представления нагружен дополнительной логикой по созданию новых модулей и реализацию переходов между ними. 128 | * При добавлении новых пунктов меню приведенный метод будет неограниченно разрастаться и может затронуть текущую реализацию. 129 | * Код тяжело покрывается модульными тестами. 130 | 131 | Поэтому при рефакторинге рассматриваемый код следует перенести в другие слои. Для этого 132 | 133 | 1. Необходимо расширить протокол выходных данных view-слоя. 134 | 135 | Следует добавить метод **«Покажи раздел меню указанного типа, перейдя с исходного view controller»** 136 | 137 | ```objective-c 138 | @protocol AGMainMenuControllerViewOutput 139 | 140 | @required 141 | - (void)showMenuSectionWithType:(MainMenuSectionType)sectionType 142 | fromViewController:(UIViewController *)viewController; 143 | 144 | @end 145 | ``` 146 | 147 | 2. Провести рефакторинг исходного метода из контроллера представления. 148 | 149 | ```objective-c 150 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 151 | NSInteger index = indexPath.row; 152 | [self.output showMenuSectionWithType:index 153 | fromViewController:self]; 154 | } 155 | ``` 156 | Теперь контроллер представления передает обработку пользовательского ввода из view-слоя в презентер, не делая ничего лишнего. 157 | 158 | 3. Расширить протокол входных данных для роутера формируемого VIPER-модуля. 159 | 160 | На каждый из трех исходных переходов следует определить свой метод. 161 | 162 | ```objective-c 163 | @protocol AGMainMenuRouterInput 164 | 165 | @required 166 | - (void)showCityFromViewController:(UIViewController *)viewController; 167 | - (void)showPlacesFromViewController:(UIViewController *)viewController; 168 | - (void)showReferenceBookFromViewController:(UIViewController *)viewController; 169 | 170 | @end 171 | ``` 172 | 173 | 4. Реализовать добавленные методы протокола выходных данных view-слоя в презентере. 174 | 175 | Получив от слоя представления данные пользовательского ввода (т.е. номер выбранной ячейки таблицы), презентер выбирает вариант перехода и передает управление роутеру. 176 | 177 | ```objective-c 178 | - (void)showMenuSectionWithType:(MainMenuSectionType)sectionType 179 | fromViewController:(UIViewController *)viewController { 180 | switch (sectionType) { 181 | case MainMenuCellsCity: 182 | [self.router showCityFromViewController:viewController]; 183 | break; 184 | case MainMenuCellsPlaces: 185 | [self.router showPlacesFromViewController:viewController]; 186 | break; 187 | case MainMenuCellsReferenceBook: 188 | [self.router showReferenceBookFromViewController:viewController]; 189 | break; 190 | } 191 | } 192 | ``` 193 | 194 | 5. Реализовать добавленные в протоколе методы в роутере формируемого VIPER-модуля. 195 | 196 | В каждом из реализуемых методов требуется загружать из фабрики нужный сториборд, из него получать следующий view controller и выполнять переход. 197 | 198 | ```objective-c 199 | - (void)showCityFromViewController:(UIViewController *)viewController { 200 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Quarters" 201 | factory:AGAppDelegateMacros.storyboardFactory 202 | bundle:nil]; 203 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agquarterslistviewcontroller"]; 204 | [viewController.navigationController pushViewController:vc animated:YES]; 205 | } 206 | 207 | - (void)showPlacesFromViewController:(UIViewController *)viewController { 208 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Places" 209 | factory:AGAppDelegateMacros.storyboardFactory 210 | bundle:nil]; 211 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agplacescategorieslist"]; 212 | [viewController.navigationController pushViewController:vc animated:YES]; 213 | } 214 | 215 | - (void)showReferenceBookFromViewController:(UIViewController *)viewController { 216 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"ReferenceBook" 217 | factory:AGAppDelegateMacros.storyboardFactory 218 | bundle:nil]; 219 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agreferencebookcategoriesviewcontroller"]; 220 | [viewController.navigationController pushViewController:vc animated:YES]; 221 | } 222 | ``` 223 | 224 | В итоге, такая реализация исходной задачи сделает код менее связным (например, можно заменить реализацию роутера и обработать переходы по-другому), тестируемым и более читаемым. Дальнейший рефакторинг кода из рассматриваемого примера, несмотря на свою необходимость, не затронет распределение логики по слоям VIPER-модуля. 225 | 226 | Результаты распределения логики по слоям VIPER-модуля представлены на рисунке ниже. 227 | 228 | ![First example result](../Resources/MVCMassacre/FirstExampleResult.png) 229 | 230 | * **Чтение данных** 231 | 232 | В этом примере в методе **viewDidLoad** рассматриваемого контроллера представления реализована загрузка списка кварталов города из локальной базы (для загрузки используется библиотека [MagicalRecord](https://github.com/magicalpanda/MagicalRecord)). При этом после загрузки на обработку передаются managed objects, что может привести к непредсказуемому поведению приложения в дальнейшем. 233 | 234 | ```objective-c 235 | - (void)viewDidLoad { 236 | [super viewDidLoad]; 237 | 238 | NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext]; 239 | NSString *sortTerm = NSStringFromSelector(@selector(priority)); 240 | NSArray *quarters = [AGQuarter MR_findAllSortedBy:sortTerm ascending:YES inContext:context]; 241 | [self handleLoadedQuarters:quarters]; 242 | } 243 | ``` 244 | 245 | При рефакторинге следует вынести логику чтения данных из view-слоя. Для этого 246 | 247 | 1. Необходимо добавить новый метод к выходным данным view-слоя. 248 | 249 | Это будет **«Получить список кварталов»**. 250 | 251 | ```objective-c 252 | @protocol AGMainMenuControllerViewOutput 253 | 254 | @required 255 | - (void)obtainQuarters; 256 | 257 | @end 258 | ``` 259 | 260 | 2. Провести рефакторинг исходного метода из контроллера представления. 261 | 262 | ```objective-c 263 | - (void)viewDidLoad { 264 | [super viewDidLoad]; 265 | [self.output obtainQuarters]; 266 | } 267 | ``` 268 | Теперь контроллер представления передает управление презентеру, избавляясь от несвойственной ему логики. 269 | 270 | 3. Реализовать добавленный в протоколе метод в презентере формируемого VIPER-модуля. 271 | 272 | ```objective-c 273 | - (void)obtainQuarters { 274 | [self.view showSpinners]; 275 | [self.interactor loadQuarters]; 276 | } 277 | ``` 278 | Презентер модуля передает запрос данных на обработку интерактору, предварительно изменив состояние view-слоя. Для этого презентер использует протокол входных данных view-слоя и просит отобразить индикатор загрузки. 279 | 280 | 4. Реализовать загрузку данных в интеракторе. 281 | 282 | ```objective-c 283 | - (void)loadQuarters { 284 | NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext]; 285 | NSString *sortTerm = NSStringFromSelector(@selector(priority)); 286 | NSArray *quarters = [AGQuarter MR_findAllSortedBy:sortTerm 287 | ascending:YES 288 | inContext:context]; 289 | NSArray *ponsoQuarters = [self createPlainObjectsFrom:quarters]; 290 | [self.output loadedQuarters:ponsoQuarters]; 291 | } 292 | ``` 293 | Необходимо загрузить данные из базы, а затем на основе полученных managed-объектов создать «плоские» модели (**PONSO, Plain Old NSObject**), которые не нагружены дополнительными возможностями и предсказуемо ведут себя при работе в потоках. 294 | 295 | 5. Передать полученные презентером данные на отображение. 296 | 297 | ```objective-c 298 | - (void)loadedQuarters:(NSArray *)quarters { 299 | [self.view hideSpinners]; 300 | [self.view handleObtainedQuarters:quarters]; 301 | } 302 | ``` 303 | Презентер просит view-слой скрыть индикаторы загрузки и обработать полученные данные. 304 | 305 | 6. Отобразить полученные данные. 306 | 307 | ```objective-c 308 | - (void)handleObtainedQuarters:(NSArray *)quarters { 309 | // Displaying quarters 310 | } 311 | ``` 312 | Непосредственная реализация в рамках примера не рассматривается. 313 | 314 | В итоге, полученная реализация исправит потенциальную ошибку, связанную с использованием managed-объектов во view-слое, уменьшит связность кода и повысит его тестируемость. 315 | 316 | Результаты распределения логики по слоям VIPER-модуля представлены на рисунке ниже. 317 | 318 | ![Second example result](../Resources/MVCMassacre/SecondExampleResult.png) 319 | 320 | * **Конфигурация объектов** 321 | 322 | В этом примере менеджер локального кеша изображений лениво создается в геттере, определенном непосредственно во view controller. 323 | 324 | ```objective-c 325 | - (id)imageLocalCacheManager { 326 | if (_imageLocalCacheManager == nil) { 327 | NSFileManager *fileManager = [NSFileManager defaultManager]; 328 | _imageLocalCacheManager = [AGImageLocalCacheManagerImplementation cacheManagerWithFileManager:fileManager]; 329 | } 330 | return _imageLocalCacheManager; 331 | } 332 | ``` 333 | 334 | Но объект сам не должен создавать требуемые ему вспомогательные объекты - это чревато дублированием кода по настройке зависимостей. Поэтому для инкапсуляции этой логики используются специальные конфигураторы, для реализации которых можно использовать готовые DI-фреймворки, например, [Typhoon](https://github.com/appsquickly/Typhoon). Тогда при рефакторинге кода этого примера необходимо сделать следующее. 335 | 336 | 1. Перенести менеджер кеша на уровень интерактора, так как взаимодействие с локальным хранилищем - это его ответственность, а не view-слоя. 337 | 338 | ```objective-c 339 | @interface AGMainMenuInteractor : NSObject 340 | 341 | @property (nonatomic, strong) id imageLocalCacheManager; 342 | 343 | @end 344 | ``` 345 | 346 | Интерактор модуля будет строгой ссылкой держать менеджер кеша. 347 | 348 | 2. Вынести настройку зависимостей объектов модуля в отдельный объект - assembly. 349 | 350 | ```objective-c 351 | @implementation AGMainMenuAssembly 352 | 353 | - (AGMainMenuInteractor *)interactorMainMenu { 354 | return [TyphoonDefinition withClass:[AGMainMenuInteractor class] 355 | configuration:^(TyphoonDefinition *definition) { 356 | [definition injectProperty:@selector(output) 357 | with:[self presenterMainMenu]]; 358 | [definition injectProperty:@selector(imageLocalCacheManager) 359 | with:[self.coreHelpersAssembly imageLocalCacheManager]]; 360 | }]; 361 | } 362 | 363 | @end 364 | ``` 365 | 366 | Так как менеджер кеша - это базовый компонент, переиспользуемый между различными модулями, хорошей практикой будет использовать для него и ему подобных сущностей отдельный конфигуратор. В примере это **coreHelpersAssembly**. 367 | 368 | 3. Добавить создание менеджера кеша в assembly для базовых объектов. 369 | 370 | ```objective-c 371 | @implementation AGCoreHelpersAssembly 372 | 373 | - (id)imageLocalCacheManager { 374 | return [TyphoonDefinition withClass:[AGImageLocalCacheManagerImplementation class] 375 | configuration:^(TyphoonDefinition *definition) { 376 | [definition useInitializer:@selector(cacheManagerWithFileManager:) 377 | parameters:^(TyphoonMethod *initializer) { 378 | [initializer injectParameterWith:[self fileManager]]; 379 | }]; 380 | }]; 381 | } 382 | 383 | @end 384 | ``` 385 | 386 | В итоге, интерактор сможет использовать менеджер кеша и передавать полученные от него данные вышележащим слоям. 387 | 388 | ```objective-c 389 | - (void)loadBackgroundImage { 390 | AGPaperGuide *guide = [AGPaperGuide MR_findFirstInContext:[NSManagedObjectContext MR_defaultContext]]; 391 | UIImage *guideBackgroundImage = [self.imageLocalCacheManager loadImageWithImageId:guide.backgroundImage.photoId]; 392 | [self.output loadedBackgroundImage:guideBackgroundImage]; 393 | } 394 | ``` 395 | 396 | Полученная реализация устраняет потенциальное дублирование при создании вспомогательных объектов и повышает тестируемость кода. 397 | 398 | Результаты распределения логики по слоям VIPER-модуля представлены на рисунке ниже. 399 | 400 | ![Third example result](../Resources/MVCMassacre/ThirdExampleResult.png) 401 | 402 | #### Общие советы, полезные при переходе от Massive-View-Controller к VIPER-модулю 403 | 404 | Ниже приведено несколько советов, полезных при переходе от Massive-View-Controller к VIPER-модулю. 405 | 406 | * Воспринимайте view в VIPER-модуле не как объект, а как слой, в котором есть множество объектов. Например, хорошим тоном будет создание в этом слое отдельных объектов, реализующих делегаты таблицы и большинство других протоколов. Также стоит использовать вспомогательные сущности для анимаций. 407 | * Классы, зависимые от **UIKit**, **Core Animation** или **Core Graphics** не должны выходить за пределы view-слоя. 408 | * Используйте во view для взаимодействия с данными простые неизменяемые объекты, реализация которых никак не связана с модельным слоем. Это гарантирует, что данные, отданные во view для отображения, уже загружены и могут быть отображены. 409 | * Сложные view, являющиеся частью иерархии view controller, должны сами реализовывать логику своего отображения. Так, кастомный date picker с дополнительными свойствами для визуализации данных может быть определен в отдельном вспомогательном классе. 410 | * Если view controller содержит очень много свойств даже после рефакторинга, стоит получившийся VIPER-модуль разбить на несколько. 411 | 412 | #### Результаты рефакторинга 413 | 414 | О правильности проведенного рефакторинга может свидетельствовать простой опросник, на все вопросы которого необходимо ответить: "Нет". 415 | 416 | * Взаимодействует ли view controller напрямую с моделью? 417 | * Содержит ли view controller бизнес-логику? 418 | * Содержит ли view controller логику, не связанную с UI? 419 | 420 | В среднем на рефакторинг одного Massive-View-Controller с учетом написания тестов уходит 2-3 рабочих дня в зависимости от опыта и квалификации разработчика. При этом после трансформации исходной реализации в VIPER-модуль решаются обозначенные выше недостатки массивного контроллера и значительно повышается понимание потоков данных и управления приложения за счет структуризации кода. 421 | 422 | Стоит отметить, что необязательно переводить все view controller проекта в VIPER-модули. Об этом необходимо задумываться, например, в тот момент, когда MVC становится трудно покрываем тестами или затрудняется переиспользование логики между классами. Это возможно, так как VIPER полностью совместим с Model-View-Controller. Т.е. внутри одного проекта могут быть как контроллеры, написанные по MVC и потенциально ожидающие рефакторинга, так и VIPER-модули. 423 | 424 | ### Дополнительное чтение 425 | 426 | * [Wikipedia: Model-View-Controller](https://ru.wikipedia.org/wiki/Model-View-Controller). 427 | * [Wikipedia: Model-View-Adapter](https://en.wikipedia.org/wiki/Model-view-adapter). 428 | * [Apple's DevPedia: Model-View-Controller](https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html#//apple_ref/doc/uid/TP40008195-CH32). 429 | * [Apple's CocoaEncyclopedia: Model-View-Controller](https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/Model-View-Controller/Model-View-Controller.html). 430 | * [Apple's documentation: UIViewController class reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/doc/uid/TP40006926). 431 | * [Apple's documentation: view controller for iOS](https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457). 432 | * [Apple's documentation: view controller design tips](https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/DesignTips.html#//apple_ref/doc/uid/TP40007457-CH5-SW1). 433 | * [WWDC 2012/236. The evolution of View Controllers on iOS](https://developer.apple.com/videos/play/wwdc2012/236/). 434 | * [WWDC 2014/229. Advanced iOS Application Architecture and Patterns](https://developer.apple.com/videos/play/wwdc2014/229/). 435 | * [iOS architecture patterns by Bohdan Orlov](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52). 436 | * [objc.io. Issue №1. Lighter view controllers](https://www.objc.io/issues/1-view-controllers/). 437 | * [Model-View-Controller (MVC) in iOS: A Modern Approach by Rui Peres](https://www.raywenderlich.com/132662/mvc-in-ios-a-modern-approach). 438 | -------------------------------------------------------------------------------- /english/mvc-chainsaw-massacre.md: -------------------------------------------------------------------------------- 1 | It is not difficult to start a new project with the VIPER architecture as a foundation. But constantly developers have to support and enhance applications, that were initially developed with chaotic code base, without considering strict rules of design and architecture. It often happens that the first version of a project doesn't have a lot of requirements and they can fit on a single sheet of paper. Therefore, there is not enough attention paid to the application architecture at this stage. But terms of reference for the second version can have as many pages as "War and Peace"(by Leo Tolstoy) has. That's why, more complex task, but an interesting one at the same time, is to migrate an existing iOS application from a weak foundation to a solid one, that is based on a flexible and robust VIPER architecture. 2 | 3 | ### Why MVC becomes Massive-View-Controller 4 | 5 | **Model-View-Controller (MVC)** is a basic architectural pattern for iOS-applications offered by Apple. Usually this pattern explicitly or implicitly is being followed by all novice developers. Model-View-Controller is obviously a three-layered architectural pattern. When using it, all application objects, depending on the purpose, belong to one of three layers: Model (Model), presentation (View) or control (Controller). Each architectural layer is separated from the other by abstract boundaries, that are used for communication between objects from the different layers. The primary purpose of this architectural concept is separation of business logic (model) from its visualization (representation). 6 | 7 | **The model layer** describes the data of an application and determines processing logic and storage. Model objects should not have a clear link with objects of the presentation layer; they don't contain any information how data could be visualized. As an example of objects of this layer can be data storage objects, parsers, network clients, etc. 8 | 9 | **The Presentation** layer contains objects that a user can see. Objects of this layer are usually can be reused. These include classes like UILabel and UIButton. 10 | 11 | **The Controller** layer mediates interaction between presentation layer objects and model objects: controls user input and uses a model and a view to implement necessary reaction. Also objects of this layer are used for definition and coordination of application problems, life cycle management of other objects. 12 | 13 | ![Model-View-Controller](../Resources/MVCMassacre/MVC.png) 14 | 15 | > **Notice**. Despite the fact that Apple in its documentation refers to the described architectural pattern as classic Model-View-Controller, however, it's correct name is **Model-View-Adapter (MVA)** or **mediating-controller MVC**. 16 | > 17 | > MVA and MVC solve the same problem, but using different approaches. Classic MVC is represented in the form of a triangle, where the vertices are layers: the model, the view and the controller - and it enables communication between the model and the view bypassing the controller. MVA also has three layers on the same line in order to prevent direct communication between the model and the view (as shown above). 18 | > 19 | > The term Model-View-Controller will be used further to avoid inconsistencies between Apple official documentation and this article. 20 | 21 | According to the Apple recommendations, **presentation controllers (view controllers)** are the core of the control layer in an iOS-app . Each such object is responsible for 22 | 23 | * view hierarchy management; 24 | * scaling of presentation views to fit a specific device display size; 25 | * updating presentation content in response to a change of data; 26 | * processing of user input and transmitting data to the model layer; 27 | * releasing of related resources in case of a insufficient memory; 28 | 29 | The recommended way to describe a view controller and related view is a Storyboard editor. Using this tool, it is possible not only to specify which presentation objects belong to a specific view controller, but also to define hierarchical relationships and transitions between different controllers. 30 | ![Storyboard editor](../Resources/MVCMassacre/Storyboard editor.png) 31 | Apple gives a few tips for creating a view controller. 32 | 33 | * **Use view controller classes provided with the standard SDK.** 34 | 35 | The iOS SDK has a set of view controllers that solve specific tasks, starting from accessing a user's contact list up to displaying media data. A good practice is to use such controllers, provided by system libraries, in your applications. 36 | 37 | * **Make a View Controller as independent as possible.** 38 | 39 | A View Controller shouldn't be aware of the internal logic of another controller or his view hierarchy. The communication between two controllers should be done through an explicitly defined public interface. 40 | 41 | * **Do not store data in a view controller.** 42 | 43 | A View controller is a mediator between the model layer and the presentation layer for data exchange. It can cache some data for quick access, validate it, but its main responsibility - to ensure that a view displays correct information. 44 | 45 | * **Use a view controller for reacting to external events.** 46 | 47 | The external events include: user input, system notifications (for example, when the keyboard appears), delegate methods for various handlers, for example [CLLocationManager](https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/index.html#//apple_ref/doc/uid/TP40007125-CH3)). 48 | 49 | These recommendations and advice, despite their usefulness, are too general, described without specifying concrete definition, therefore have a number of disadvantages. The major of them is uncontrolled growth of complexity of view controllers. Their tight coupling with the view life-cycle, which is the specific characteristic of the iOS SDK, requires generation of a response to events of this loop directly in the view controller. Implementation of handling of external events, user input is also happens in a view controller. Furthermore, a model often becomes too passive and is used exclusively for data access, but all the business logic still resides in a view controller. It worth mentioning that Apple almost does not describe important organizational problems of bi-directional data transfer between controllers, configuration of created controllers and so on, as a result a majority of developers think that these are responsibilities of the view controller. 50 | 51 | As a result, view controllers become a core of almost everything that happens in an application and, as a consequence, grow to giant size, turning into something called as **Massive-** or **Mega-View-Controller**. Similar type of object is an excellent example of the consequences of a poorly-designed development process that ignores the basic principles of design and architecture. 52 | 53 | ![Massive-View-Controller](../Resources/MVCMassacre/MassiveViewControllerScheme.png) 54 | 55 | The main disadvantages of using Massive-View-Controller are : 56 | 57 | * **High complexity of support and development.** 58 | 59 | The Massive-View-Controller code is difficult to modify and extend due to its tight coupling and unpredictable data streams. It is always risky to make changes, because this can result in a broken of existing functionality of an application, but more important this might be not recognized until a certain point in time. 60 | 61 | * **A high entry threshold.** 62 | 63 | It can be a daunting task to find a required method in a such large amount of code: code structure is not obvious and requires time for exploring. 64 | 65 | * **Poorly testable code.** 66 | 67 | Due to the tight coupling Massive-View-Controller code is not coverable by unit tests. Thus, an attempt to check presentation logic leads to an explicit life-cycle method call that can indirectly lead to the instantiation of all related views, that violates the principles of unit-testing. 68 | 69 | * **It is almost impossible to reuse code.** 70 | 71 | Implementation of new application features is carried out directly in a view controller. That's why it is impossible to reuse this part of the code without reusing the entire controller. As well as refactoring and decomposition of such large amount of code can be a non-trivial task. 72 | 73 | To avoid these drawbacks and prevent an application development process to result in a stalemate, it is necessary to strictly follow the requirements of a well-designed architectural pattern. Model-View-Controller, despite the weaknesses and non-obvious solutions to some design problems, due to the ease of use, it is still suitable for smaller applications or applications that require high speed of development. However, for large and complex projects, with long-term support and development cycle is better to use a more flexible architecture - VIPER. 74 | 75 | ### From poor implementation to a good one. Massive-View-Controller refactoring. 76 | 77 | Faced with the task of developing an application whose codebase is burdened with the Massive-View-Controller, first of all you have to improve the structure of the project. As a result, this requires migration from a large and hardly maintainable Massive-View-Controller into something more convenient and flexible, for example, a VIPER-module. 78 | In the VIPER, as opposed to MVC, the view controller is the core of the view-layer, that allows not to burden it with additional logic and utilize it only for implementation of the tasks directly related to data visualization. In this case, a view controller, receiving various external events (user input, system notifications of insufficient memory, etc.) passes them for processing to the lower layer - Presenter. All other non-visual module logic, implemented in MVC directly in a view controller (eg, creation of other modules or data transfer between them), is incapsulated in other layers as well. 79 | 80 | There are three examples of transition from a Massive-View-Controller to a VIPER-module described further. A ViewController implementation from a real project (a guide to one of the cities in the world) is used as a starting point. This view controller contains the main navigation menu implementation. 81 | 82 | ![Storyboard editor](../Resources/MVCMassacre/MassiveViewControllerExample.png) 83 | 84 | #### Refactoring examples 85 | 86 | * **Transition between modules** 87 | 88 | This code snippet implements a transition to an inner section of the app. The direction of the transition is determined by the index of the selected menu table cell. 89 | 90 | ```objective-c 91 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 92 | long index = indexPath.row; 93 | switch (index) { 94 | case MainMenuCellsCity: { 95 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Quarters" 96 | factory:AGAppDelegateMacros.storyboardFactory 97 | bundle:nil]; 98 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agquarterslistviewcontroller"]; 99 | [self.navigationController pushViewController:vc 100 | animated:YES]; 101 | } 102 | break; 103 | case MainMenuCellsPlaces: { 104 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Places" 105 | factory:AGAppDelegateMacros.storyboardFactory 106 | bundle:nil]; 107 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agplacescategorieslist"]; 108 | [self.navigationController pushViewController:vc 109 | animated:YES]; 110 | } 111 | break; 112 | case MainMenuCellsReferenceBook: { 113 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"ReferenceBook" 114 | factory:AGAppDelegateMacros.storyboardFactory 115 | bundle:nil]; 116 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agreferencebookcategoriesviewcontroller"]; 117 | [self.navigationController pushViewController:vc 118 | animated:YES]; 119 | } 120 | break; 121 | } 122 | } 123 | ``` 124 | 125 | This implementation has a number of issues. 126 | 127 | * Presentation layer is burdened with additional logic for creating new modules and implementing transitions between them. 128 | * If you continue to add new menu items into this method it will grow indefinitely, and may affect the current implementation. 129 | * It not easy to cover this code by unit tests. 130 | 131 | Therefore, while refactoring this code should be moved to other layers. There are several steps to achieve this 132 | 133 | 1. We need to extend an output data protocol of the view-layer. 134 | 135 | It is necessary to add a method that can be described as **"Show a menu item of the specified type, with transitioning from the source view-controller"** 136 | 137 | ```objective-c 138 | @protocol AGMainMenuControllerViewOutput 139 | 140 | @required 141 | - (void)showMenuSectionWithType:(MainMenuSectionType)sectionType 142 | fromViewController:(UIViewController *)viewController; 143 | 144 | @end 145 | ``` 146 | 147 | 2. Refactor the initial method of the view controller. 148 | 149 | ```objective-c 150 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 151 | NSInteger index = indexPath.row; 152 | [self.output showMenuSectionWithType:index 153 | fromViewController:self]; 154 | } 155 | ``` 156 | Now the view controller delegates the processing of user input from the view-layer to the presenter, without doing anything extra. 157 | 158 | 3. Extend the input data protocol for the router of the VIPER-module that is being created. 159 | 160 | Each of the three initial transitions should have its own method defined. 161 | 162 | ```objective-c 163 | @protocol AGMainMenuRouterInput 164 | 165 | @required 166 | - (void)showCityFromViewController:(UIViewController *)viewController; 167 | - (void)showPlacesFromViewController:(UIViewController *)viewController; 168 | - (void)showReferenceBookFromViewController:(UIViewController *)viewController; 169 | 170 | @end 171 | ``` 172 | 173 | 4. Implement methods, which have been just added to the view-layer output data protocol, inside the presenter. 174 | 175 | After getting information about user input (ie, the selected table cell number) from the view layer, the presenter chooses a transition and transfers the control to the router. 176 | 177 | ```objective-c 178 | - (void)showMenuSectionWithType:(MainMenuSectionType)sectionType 179 | fromViewController:(UIViewController *)viewController { 180 | switch (sectionType) { 181 | case MainMenuCellsCity: 182 | [self.router showCityFromViewController:viewController]; 183 | break; 184 | case MainMenuCellsPlaces: 185 | [self.router showPlacesFromViewController:viewController]; 186 | break; 187 | case MainMenuCellsReferenceBook: 188 | [self.router showReferenceBookFromViewController:viewController]; 189 | break; 190 | } 191 | } 192 | ``` 193 | 194 | 5. Implement methods added to the protocol inside the router of the VIPER-module which is being created. 195 | 196 | In each method being implemented, it necessary to load the required storyboard from the factory, get a next view controller from it and perform a Segue. 197 | 198 | ```objective-c 199 | - (void)showCityFromViewController:(UIViewController *)viewController { 200 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Quarters" 201 | factory:AGAppDelegateMacros.storyboardFactory 202 | bundle:nil]; 203 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agquarterslistviewcontroller"]; 204 | [viewController.navigationController pushViewController:vc animated:YES]; 205 | } 206 | 207 | - (void)showPlacesFromViewController:(UIViewController *)viewController { 208 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"Places" 209 | factory:AGAppDelegateMacros.storyboardFactory 210 | bundle:nil]; 211 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agplacescategorieslist"]; 212 | [viewController.navigationController pushViewController:vc animated:YES]; 213 | } 214 | 215 | - (void)showReferenceBookFromViewController:(UIViewController *)viewController { 216 | UIStoryboard *sb = [TyphoonStoryboard storyboardWithName:@"ReferenceBook" 217 | factory:AGAppDelegateMacros.storyboardFactory 218 | bundle:nil]; 219 | UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"agreferencebookcategoriesviewcontroller"]; 220 | [viewController.navigationController pushViewController:vc animated:YES]; 221 | } 222 | ``` 223 | 224 | As a result, such implementation of the original problem will make code loosely coupled (for example, you can replace the router implementation and handle transitions differently), testable and more readable. Further code refactoring of this example, despite its necessity, will not affect logic segregation across the VIPER-module layers. 225 | 226 | The results of logic segregation across the VIPER-module layers are shown in the figure below. 227 | 228 | ![First example result](../Resources/MVCMassacre/FirstExampleResult.png) 229 | 230 | * **Reading data** 231 | 232 | In this example loading of a list of city quarters from the local database ([MagicalRecord](https://github.com/magicalpanda/MagicalRecord) library is used for loading) is implemented in the **viewDidLoad** method of the view controller being reviewed. In this case after loading, managed objects are passed for processing, that can lead to unpredictable behavior of an app in future. 233 | 234 | ```objective-c 235 | - (void)viewDidLoad { 236 | [super viewDidLoad]; 237 | 238 | NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext]; 239 | NSString *sortTerm = NSStringFromSelector(@selector(priority)); 240 | NSArray *quarters = [AGQuarter MR_findAllSortedBy:sortTerm ascending:YES inContext:context]; 241 | [self handleLoadedQuarters:quarters]; 242 | } 243 | ``` 244 | 245 | While refactoring all data reading logic should be moved out of the view layer.For this 246 | 247 | 1. A new method should be added to the output protocol of the view-layer. 248 | 249 | In this case it can be read as **"Obtain a list of city quarters."** 250 | 251 | ```objective-c 252 | @protocol AGMainMenuControllerViewOutput 253 | 254 | @required 255 | - (void)obtainQuarters; 256 | 257 | @end 258 | ``` 259 | 260 | 2. Carry out refactoring of the original method from the view controller. 261 | 262 | ```objective-c 263 | - (void)viewDidLoad { 264 | [super viewDidLoad]; 265 | [self.output obtainQuarters]; 266 | } 267 | ``` 268 | Now the view controller transfers control to the presenter, getting rid of logic that is not typical for it. 269 | 270 | 3. Implement the method added to the protocol inside the presenter of VIPER-module being formed. 271 | 272 | ```objective-c 273 | - (void)obtainQuarters { 274 | [self.view showSpinners]; 275 | [self.interactor loadQuarters]; 276 | } 277 | ``` 278 | The module presenter transmits a data request to an interactor for processing, after changing the state of view-layer. To do this, the presenter uses the view-layer input protocol, and requests to display a loading progress bar. 279 | 280 | 4. Implement data loading inside the interactor. 281 | 282 | ```objective-c 283 | - (void)loadQuarters { 284 | NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext]; 285 | NSString *sortTerm = NSStringFromSelector(@selector(priority)); 286 | NSArray *quarters = [AGQuarter MR_findAllSortedBy:sortTerm 287 | ascending:YES 288 | inContext:context]; 289 | NSArray *ponsoQuarters = [self createPlainObjectsFrom:quarters]; 290 | [self.output loadedQuarters:ponsoQuarters]; 291 | } 292 | ``` 293 | It is necessary to load data from the database, and then on the basis of received managed-objects create "flat" models (**PONSO, Plain Old NSObject**), which are not overloaded with extra features and predictably behave in a concurrent environment. 294 | 295 | 5. Pass the data received by the presenter for displaying. 296 | 297 | ```objective-c 298 | - (void)loadedQuarters:(NSArray *)quarters { 299 | [self.view hideSpinners]; 300 | [self.view handleObtainedQuarters:quarters]; 301 | } 302 | ``` 303 | Presenter asks the view-layer to the hide progress bar and process the data. 304 | 305 | 6. Displaying received data. 306 | 307 | ```objective-c 308 | - (void)handleObtainedQuarters:(NSArray *)quarters { 309 | // Displaying quarters 310 | } 311 | ``` 312 | Concrete implementation is not covered in this example. 313 | 314 | As a result, new implementation will prevent the potential error associated with the use of managed-object in the view-layer, reduce code coupling and improve its testability. 315 | 316 | The results of the logic segregation across the VIPER-module layers are shown in the figure below. 317 | 318 | ![Second example result](../Resources/MVCMassacre/SecondExampleResult.png) 319 | 320 | * **Objects configuration** 321 | 322 | In this example, the local image cache manager is lazily instantiated in the getter method, defined directly in the view controller. 323 | 324 | ```objective-c 325 | - (id)imageLocalCacheManager { 326 | if (_imageLocalCacheManager == nil) { 327 | NSFileManager *fileManager = [NSFileManager defaultManager]; 328 | _imageLocalCacheManager = [AGImageLocalCacheManagerImplementation cacheManagerWithFileManager:fileManager]; 329 | } 330 | return _imageLocalCacheManager; 331 | } 332 | ``` 333 | 334 | But an object should not create needed helper objects by itself - it is fraught with duplication of the code for setting up dependencies. Therefore, special configurators are used to encapsulate this logic, you can use exisiting DI-frameworks for their implementation, for example, [Typhoon](https://github.com/appsquickly/Typhoon). In this instance for refactoring of this example code, you have to do the following 335 | 336 | 1. Move the cache manager to the interactor layer as far as interaction with the local storage — this is it's responsibility, not the view-layer's. 337 | 338 | ```objective-c 339 | @interface AGMainMenuInteractor : NSObject 340 | 341 | @property (nonatomic, strong) id imageLocalCacheManager; 342 | 343 | @end 344 | ``` 345 | 346 | The module interactor will keep a strong reference to the cache manager. 347 | 348 | 2. Move configuration of module dependencies into a separate object — assembly. 349 | 350 | ```objective-c 351 | @implementation AGMainMenuAssembly 352 | 353 | - (AGMainMenuInteractor *)interactorMainMenu { 354 | return [TyphoonDefinition withClass:[AGMainMenuInteractor class] 355 | configuration:^(TyphoonDefinition *definition) { 356 | [definition injectProperty:@selector(output) 357 | with:[self presenterMainMenu]]; 358 | [definition injectProperty:@selector(imageLocalCacheManager) 359 | with:[self.coreHelpersAssembly imageLocalCacheManager]]; 360 | }]; 361 | } 362 | 363 | @end 364 | ``` 365 | 366 | As far as the cache manager is a basic component and is reused between different modules, the good practice is to use separate configurator for it and similar entities. In this example it is **coreHelpersAssembly**. 367 | 368 | 3. Add creation of the cache manager into the assembly for base objects. 369 | 370 | ```objective-c 371 | @implementation AGCoreHelpersAssembly 372 | 373 | - (id)imageLocalCacheManager { 374 | return [TyphoonDefinition withClass:[AGImageLocalCacheManagerImplementation class] 375 | configuration:^(TyphoonDefinition *definition) { 376 | [definition useInitializer:@selector(cacheManagerWithFileManager:) 377 | parameters:^(TyphoonMethod *initializer) { 378 | [initializer injectParameterWith:[self fileManager]]; 379 | }]; 380 | }]; 381 | } 382 | 383 | @end 384 | ``` 385 | 386 | As a result,the interactor is able to use the cache manager and transmit the data received from it to upper layers. 387 | 388 | ```objective-c 389 | - (void)loadBackgroundImage { 390 | AGPaperGuide *guide = [AGPaperGuide MR_findFirstInContext:[NSManagedObjectContext MR_defaultContext]]; 391 | UIImage *guideBackgroundImage = [self.imageLocalCacheManager loadImageWithImageId:guide.backgroundImage.photoId]; 392 | [self.output loadedBackgroundImage:guideBackgroundImage]; 393 | } 394 | ``` 395 | 396 | The resulting implementation eliminates potential problem of duplication while creating helper objects and increases code testability. 397 | 398 | The results of the logic segregation across the VIPER-module layers are shown in the figure below. 399 | 400 | ![Third example result](../Resources/MVCMassacre/ThirdExampleResult.png) 401 | 402 | #### General useful tips to consider when moving from a Massive-View-Controller to a VIPER-module 403 | 404 | Here are some tips that are useful when moving from Massive-View-Controller to a VIPER-module. 405 | 406 | * Think of a view in a VIPER-module not as an object but as a layer which contains a lot of objects. For example, a good manner is to create separate objects in this layer that implement table delegates and most of other protocols. Also it is necessary to use the auxiliary entities for animations. 407 | * Classes dependent on **UIKit**, **Core Animation** or **Core Graphics** should not go beyond the view-layer. 408 | * Use simple immutable objects in the view layer to interact with data, the implementation of which has no connection with the model layer. This ensures that data passed to the view layer for displaying has been already pre-loaded and can be displayed. 409 | * Complex views, that are part of a view controller's hierarchy, should implement display logic themselves. As an example, a custom date picker with additional properties for data visualization can be defined in a separate helper class. 410 | * If a view controller has a lot of responsibilities even after refactoring, a resulting VIPER-module have to be divided into several modules. 411 | 412 | #### Refactoring results 413 | 414 | If you answer "No" to the questions in this simple questionnaire, it probably indicates that the refactoring has been done correctly. 415 | 416 | * Whether a view controller interacts directly with a model? 417 | * Does a view controller contain any business logic? 418 | * Whether a view controller contains logic that is not related to the UI? 419 | 420 | On average, refactoring of a single Massive-View-Controller, including writing test, takes 2-3 working days depending on experience and qualifications of a developer. In this case, after transformation of initial implementation into a VIPER-module, problems of Massive-View-Controller mentioned above are solved and understanding of data flow and control is greatly improved by code structuring. 421 | 422 | It should be noted that it is not necessarily to transform all view controllers of a project into VIPER-modules. This should be considered, for example, at the moment when it becomes difficult to cover an MVC class by tests or it is difficult to reuse logic between classes. This is possible since VIPER is fully compatible with Model-View-Controller. Those, within the same project there can be controllers written in the MVC style, which are potentially waiting for refactoring, as well as VIPER-modules. 423 | 424 | ### Additional reading 425 | 426 | * [Wikipedia: Model-View-Controller](https://ru.wikipedia.org/wiki/Model-View-Controller). 427 | * [Wikipedia: Model-View-Adapter](https://en.wikipedia.org/wiki/Model-view-adapter). 428 | * [Apple's DevPedia: Model-View-Controller](https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html#//apple_ref/doc/uid/TP40008195-CH32). 429 | * [Apple's CocoaEncyclopedia: Model-View-Controller](https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/Model-View-Controller/Model-View-Controller.html). 430 | * [Apple's documentation: UIViewController class reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/doc/uid/TP40006926). 431 | * [Apple's documentation: view controller for iOS](https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457). 432 | * [Apple's documentation: view controller design tips](https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/DesignTips.html#//apple_ref/doc/uid/TP40007457-CH5-SW1). 433 | * [WWDC 2012/236. The evolution of View Controllers on iOS](https://developer.apple.com/videos/play/wwdc2012/236/). 434 | * [WWDC 2014/229. Advanced iOS Application Architecture and Patterns](https://developer.apple.com/videos/play/wwdc2014/229/). 435 | * [iOS architecture patterns by Bohdan Orlov](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52). 436 | * [objc.io. Issue №1. Lighter view controllers](https://www.objc.io/issues/1-view-controllers/). 437 | * [Model-View-Controller (MVC) in iOS: A Modern Approach by Rui Peres](https://www.raywenderlich.com/132662/mvc-in-ios-a-modern-approach). 438 | --------------------------------------------------------------------------------