Михаил Мужев

VIPER Swift

Что такое VIPER?
Модель представляет собой разделение обычного MVC подхода на несколько отдельных частей. Эти части называются View, Interactor, Presenter, Entity, Router.

View — отвечает за представление на экране, т. е. за определенный набор различных UIView. View не самостоятелен и не несет в себе никакой бизнес-логики.
Interactor — отвечает за логику приложения, т. е. за какие-то расчеты, операции с объектами и т. п.
Presenter — отвечает за передачу данных во View: когда это сделать и каким образом.
Entity — класс-сущность, над которым будут производиться операции и преобразования. Например, класс Human, у которого есть свойства age, weight, height. Entity, как и View, не может нести в себе никакой бизнес-логики.
Router — отвечает за маршрутизацию модулей, т. е. осуществляет навигацию между экранами приложения.

Interactor не должен контактировать с View и наоборот. Также Interactor ничего не знает о существовании Router. Presenter выступает своеобразным связующим звеном между ними.

Между элементами модуля передаются только простые стандартные объекты, такие как String, Int, NSObject. Передача таких объектов, как экземпляр UIView, недопустима. Части модуля должны быть спрятаны за протоколами, что обеспечивает высокий уровень абстракции.
Зачем это нужно
1) Упрощает добавление или исправление кода — из-за разделения обязанностей методы получаются атомарными или близкими к этому состоянию, следовательно, для добавления/исправления функциональности не требуется переписывать все приложение, а лишь поработать с отдельными методами. Это существенно ускоряет разработку.

2) Делает код более тестируемым, опять же из-за того, что методы более атомарные — они перестают быть черным ящиком с большим количеством исходов теста.
Проблемы
На бумаге всё выглядит очень просто и понятно, однако на деле приходится сталкиваться с трудностями.

Первая и основная проблема это установление зависимостей между частями модуля (между Presenter, Interactor и т. п.). Подходов к решению этой проблемы существует два:
1) прописывание всех зависимостей вручную
2) использование библиотек для инъекции зависимостей (например, Typhoon).

Первый вариант является наиболее сложным и затратным по времени. Нужно всё верно связать и сделать так, чтобы не было утечек памяти. Из-за чего появляются утечки? Из-за двойных связей — Router имеет ссылку на Presenter, а Presenter, в свою очередь, на Router. Точно так же обстоит дело с View и Presenter. Для того чтобы избежать этой проблемы, нужно в одной из связей использовать weak-ссылку на объект (Presenter на View, Presenter на Router). Это позволит уйти от циклической зависимости и уничтожить объект, когда он будет больше не нужен. Главный плюс данного метода — всё прозрачно: вы полностью контролируете время жизни объектов.

Использование сторонних библиотек должно упростить жизнь разработчику и позволить ему не переживать за зависимости. Однако появляется другая проблема — сторонние библиотеки имеют свойство меняться или вообще исчезать. Учитывая, что данная библиотека будет отвечать за связность проекта, ее потеря будет катастрофической, придется либо искать другую библиотеку, либо делать всё вручную. При изменении библиотеки возможно придется что-то переписывать в коде. В любом случае, это займет немало времени.

Другая беда, приходящая при использовании VIPER, заключается в том, что многие сторонние библиотеки и некоторые стандартные средства языка (UITableView) не пригодны для этой модели. Чаще всего нарушается принцип единой ответственности. С одним из таких нарушений нам пришлось столкнуться на нашем проекте: до перевода на VIPER мы использовали библиотечный textField, в котором был метод, проверяющий, является ли содержимое email адресом. Вызывался он обращением к экземпляру этого класса. Получалось, что логика находилась внутри раздела View. Решений было несколько:
 — пренебречь принципом единой ответственности и оставить всё как есть;
 — передать этот textField в Interactor, но это непростой объект, следовательно, снова нарушение VIPER;
 — избавиться от этой библиотеки и написать проверку самому внутри Interactor.

В данной ситуации это маленькая проблема, которая решилась довольно быстро (мы пошли последним путем, написав всё сами), однако бывают ситуации куда более серьезные, требующие бо́льших умственных и временных затрат.
Инъекция зависимостей
Mutual mobile предложили объединить все зависимости в одном методе и вызывать его при запуске приложения в классе AppDelegate. Этот подход имеет два серьезных недостатка:
1) при разработке приложения, у которого довольно объемный код, метод с зависимостями будет неприлично большим и тяжело читаемым.
2) при исполнении такого метода вся эта куча зависящих друг от друга объектов будет висеть в памяти с самого начала работы приложения и занимать достаточное её количество.

С первым вопросом всё не так сложно — достаточно разделить большой метод на несколько маленьких, каждый из которых будет отвечать за конкретный модуль. Со вторым всё гораздо интереснее. Наиболее удачным решением является создание зависимостей не раньше момента обращения к модулю с этими зависимостями (lazy initialization).

Для каждого модуля создали класс Assembly, в который поместили метод assembleModule():
Данный метод собирает все зависимости относительно этого модуля и возвращает Router.

Пример:
Допустим, имеется два модуля: Start и Options. Start появляется первым, затем, при нажатии на кнопку, переходим к Options. Прописываем при запуске приложения зависимости, вызывая StartAssembly. assembleModule(). Затем создаем в StartRouter метод injectOptionsDependencies():
Следующим шагом вызываем метод injectOptionsDependencies() в методе presentOptions(), который переведет нас на экран Options.
Таким образом, в памяти больше не лежит ничего лишнего.
Использование UINavigationController для перехода между модулями
В качестве средства для навигации мы используем UINavigationController. Для этого в Router создаем метод pushViewController:
Зависимости распределяются следующим образом:
NavigationController держит сильную ссылку на View, View на Presenter, а Presenter на Interactor и Router. Получается, что NavigationController держит весь модуль. Таким образом, данное решение позволяет нам уничтожить весь модуль сразу после того, как ViewController будет убран с NavigationController.
TableViewDelegate и TableViewDataSource
Данный метод не является единственно верным и был создан нами за неимением других способов работы с TableView. Проиллюстрируем на примере.

Существует массив объектов Human, приходящих асинхронно с помощью какого-то API.
Human имеет следующие свойства:
 — Name: String;
 — Age: Int;
 — Photo: UIImage;
Для отображения контента на устройстве используется UITableView. В ячейку загружаются имя, возраст и фото человека из массива [Human].

Один из принципов работы с VIPER говорит, что View должен быть пассивен, а Presenter не должен возвращать View-уровню ничего. У View должны быть сеттеры, через которые Presenter будет выводить на экран данные. Проблема в том, что это работает для объектов, которые уже созданы, а не для создающихся динамически. Т. е. нельзя прописать сеттеры для каждой ячейки, т. к. во-первых, на этапе написания кода неизвестно, сколько их придет, во-вторых, глупо писать одни и те же сеттеры для одинаковых объектов. Решение: создаем по переменной для каждого свойства класса внутри View-уровня, добавляем для каждой переменной сеттер.
В Presenter создаем метод, который будет брать данные из Interactor и класть во View-уровень в переменные «currentHuman»
Таким образом, каждая ячейка будет заполняться нужной информацией. Это нарушает принцип того, что View не должен иметь ничего, кроме ссылок на элементы UIView и ссылки на Presenter. Но мы соблюли остальные принципы, и код всё еще удобен для тестирования.
Передача объектов между модулями
По каким-то неизвестным причинам никто не описал этот процесс, хотя он является актуальным, и, так или иначе, с ним придется столкнуться. Покажем пример со списком людей: предположим, у класса Human есть не только поля "имя", "возраст", "фото", но и "биография", "адрес проживания". Т. е. при нажатии на ячейку таблицы с человеком происходит переход на другой экран с более подробной информацией. Этот другой экран является другим модулем, соответственно, надо передать туда выбранного человека.

Принципы VIPER гласят, что модули могут взаимодействовать друг с другом только через Wireframe. Тут мы сталкиваемся с очередной проблемой — объект Human находится внутри Interactor, а этот уровень, в свою очередь, не контактирует напрямую с Wireframe: между ними стоит Presenter. А следом еще одна беда — между частями модуля нельзя передавать «сложные» объекты. Что сделали мы: придерживаясь того, что все объекты в Swift должны являться наследниками NSObject, мы «заворачиваем» объект Human в NSObject и забираем его из Interactor:
— затем в Interactor модуля-получателя создаем сеттер, в котором будем «разворачивать» объект:
— также прописываем сеттер для Presenter модуля-получателя:
— то же самое для Wireframe модуля-получателя
— затем делаем в Wireframe отправителя сеттер:
— и последний штрих — отправка из Presenter модуля-отправителя
В итоге, пока объект шел, для всех частей модулей, кроме Interactor, он был простым объектом. Таким образом, мы не показали детали реализации класса Human, тем самым абстрагировавшись от него. Теперь для изменения объекта пересылки потребуется переделать только геттер в Interactor отправителя и сеттер в Interactor получателя.
Спасибо за внимание!
BytePace © Все права защищены