https://habrahabr.ru/post/326620/- Разработка под iOS
- Swift
- Objective C
Хочу поделится довольно обычной, но показательной историей. Идея проекта появилась 3 месяца назад, за 1 месяц была реализована и вот уже два месяца как проект переодически висит в топе GitHub, попал в какие только можно профильные новостные ресурсы, и даже забрался в дайджест в статье “Топ 5 библиотек апреля”.
Вы могли подумать что я хвалюсь, но нет. Это предыстория нужна для более глубоко диссонанса. Я хочу поговорить об…
архитектуре. Да, знаю-знаю, “
сколько можно” и “
что он себе позволяет”. Но я буду говорить не столько о паттернах, сколько о подходе к их использованию. Именно такие статьи я искал и люблю. Примеров синглтона и фабрики вы найдете больше, чем ошибок при выходе новой версии свифта, а мы поговорим об обобщенном подходе на примере моей
библиотеки.
Перед погружением — прочтите инструкцию
Не сказал бы что я iOs-разработчик какого-то запредельного уровня, поэтому прошу отнестись к всему сказанному
критично. Уверен есть люди опытнее меня, для них все очевидно. А вот для заблудшей души, вчера сортировавшей массивы, хорошо бы быть объективным. Я буду стараться.
Задраить люки! Погружаемся!
Проект упрощает работу с разрешениями. Помимо этого повышает конверсию на получение тех же нотификаций. Кому не нравится красивое диалоговое окно?)
Основные требования к проекту:
- Простое внедрение
- Простое использование
- Удобная кастомизация и расширение (если вдруг захочется добавить новых разрешений или визуалок)
В остальном просто попытался сделать хорошо (к примеру, возможность сменить бизнес-логику не стояла приоритетной, но учитывалась)
А теперь давайте рассуждать. Это вообще штука полезная. Чтобы проект получился простым — нужно иметь простой интерфейс и не грузить программиста реализацией под капотом. В этом я вдохновлялся подходом от Appodeal. В общем, нужно иметь одну точку входа. Т.е. сконфигурировали объект 2-мя разрешениями, а далее запросили их. Должно быть так же просто, как это звучит!
Сразу дьявол на левом? плече шепчет: “Singleton…”. И первые дни мне казалось это прекрасным решением, в AppDelegate сконфигурировал, а показывай контролер где захочется.
Но проблем оказалось больше:
- Крайне редко необходимо будет держать в памяти диалоговое окно
- После получения всех разрешений точно не нужно держать его в памяти
- Мало вероятно что будет нужно запрашивать одни и те же разрешения на разных экранах
В общем, паттерн имел больше минусов, чем плюсов. Тут я хочу обратить внимание на то что отталкивался именно от вариантов использования своего проекта. Уверен что и Singleton оправдан в использовании, но в других ситуациях.
В переломный момент мою точку зрения подтвердил известный в определенных кругах
iOS уже Android разработчик — Алексей Скутаренко, назвав паттерн “сомнительным”. То ли от нелюбви к этому паттерну, то ли от не лучший применимости его к моим потребностям — неизвестно. Но решение было принято — выбросить листов 20 макулатуры, достать новых. Маркер, собственно, тоже кончился.
Тогда было принято решение пойти от обратного. Как я бы хотел, чтобы использовали проект? Я это четко представлял:
class ViewController: UIViewController {
var permissionAssistant = SPRequestPermissionAssistant.modules.dialog.interactive.create(with: [.Camera, .PhotoLibrary, .Notification])
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.permissionAssistant.present(on: self)
}
}
Решение напросилось само собой: должен быть главный класс, его мы и назовем PermissionAssistant. А логику разделим на ключевые блоки, для удобства объединим их словом Manager. А что, логично, разные задачи — соответственно разные классы будут за них ответственные.
Теперь давайте определимся с тем, какие функциональные части будут. Очевидно, одна из будет отвечать за запрос разрешений и получение информации о них. Назовем ее PermissionsManager. Так как подразумевается еще и визуальная часть, добавим PresenterManager (своруем именование у Viper, да будут в достатке его открыватели).
Презентер будет отвечать за презентацию контроллера, его конфигурирование… вообще за UI (если оно, конечно, будет). Кстати, обращаю внимание, что все части скрываем протоколами для большей гибкости в дальнейшем.
Гибкости?!
Да. Может не лучшее слово, но отражает суть. Представим что мы ремонтируем подводную лодку, крепление винта — 16-листовая резьба с 29 дюймами (только что выдумал). Не нужно каждый раз делать новую подводную лодку, достаточно будет сделать винт с известными требованиями и прикрутить его. Сделать и прикрутить.
Не мастер я алегорий, давайте к примеру с кодом. Разберём PermissionsManager, его сокрытие протоколом и реализацию. Для начала определим функционал менеджера. Нам хватит двух методов:
- Запрос разрешения
- Одобрено ли разрешение
По вкусу можно функционал расширить, но нам хватит. И так, протокол:
protocol PermissionManagerInterface {
func requestPermission(_ type: PermissionType)
func isAllowedPermission(_ type: PermissionType)
}
В нашем случае это требования к винту лодки.
Теперь имплементируем протокол. Получим реальный объект (винт). Его и прикручиваем. А вот наш главный класс Assistant (подводная лодка) не будет знать какая конкретно реализация (из какого метала, сколько дней ее лили и сколько работа стоила). Главный класс знает только что есть две функции. Захотели сменить реализацию — пожалуйста) Особенно полезно это будет в кастомизации визуальной части и DataSource. Вот о нем сейчас и поговорим.
Очевидно что визуальная часть куда сложнее, чем просто Presenter. По хорошему ее нужно делить на модули. Собственно разделим на две части: Controller и DataSource
Presenter держит контроллер, он будет разбираться с жестами, экраном и прочим, чем занимаются контроллеры. Конечно, возникает вопрос как контроллер будет сообщать о действиях. У особо пытливых возникнет вопрос «а ARC часом не уничтожит все к чертям?».
Если с первым вопросом все понятно — делегаты, то второй — проблема, которая остановила мою работу на неделю. Но чтобы вы понимали, я дополнил схему ссылками между объектами (надеемся все знают про сильные и слабые ссылки, вопросов возникнуть не должно).
Проблема очевидна (надеюсь) — контроллер держит исключительно объект Assistant (конечно, Presenter, но он держит Assistant, так что опустим звено). Если проблема еще не понятна, разъясняю:
Представим что объект Assistant вышел из своей области видимости, и соответственно, был выкинут за борт ARC. Если не был презентован контроллер, то умирает весь объект целиком. Это корректное поведение. Но если контролер был презентован…
то он теперь весит в стеке — и соответственно на него имеется ссылка вне объекта. А вот Assistant, так как выйдет из зоны видимости — умрет. Продемонстрировать можно на простом примере
if true {
let permissionAssistant = SPRequestPermissionAssistant.modules.dialog.interactive.create(with: [.Camera, .PhotoLibrary, .Notification])
permissionAssistant.present(on: self)
}
Получаем ситуацию, когда контроллер будет жив, а вот все классы окружения — умрут. И даже Presenter, который и держал контроллер.
Грустно
— подумал я, и начал глубже погружаться в кучу схем, читать про паттерны (может упустил какой) и вообще выпал из мира сего. Погрузился в плаванье.
Сколько разговоров было о проблеме, на уши поднял даже своих
матросов сотрудников. Все в один голос твердили — “
Что за ужасная архитектура?!”, “
Автору — яду” и “
Контроллер должен все держать”.
Да, контроллер я выносил в центр. Но проблема в том, что если контроллер держит Assistant и небыл презентован сразу при инициализации — умирает весь объект. В общем, перевернуть связи не получилось, а это означало что…
контроллер выносить как главный объект! Писать логику внутри контроллера — ну уж нет.
Решение пришло само собой за чашечкой чая и было чем то вроде прозрения:
— “А почему нет?”
Просто инициализировать Assistant как проверти контроллера — и все! Пока жив родитель, жив и Assistant. А так как все диалоговые контролеры подразумевались модальными, решение отлично влилось. Такое решение мне показалось оптимальным, хоть и педантичность внутри взгрустнула. Что ж, продолжим. Дух был поднят, снова набираем скорость!
Теперь хорошо бы разделить UI и PermissionManager. Тут все тривиально — делаем протокол PermissionInterface, который выглядит так:
protocol PermissionInterface {
func isAuthorized() -> Bool
func request(withComlectionHandler complectionHandler: @escaping ()->()?)
}
И для каждого нового пермишина (Location, Notification, Camera…) реализовываем его. А в PermissionManager создаем необходимый класс Permission и дергаем нужные функции.
Обновим схему:
Теперь мы видим всю картину. И как видно, любой кирпич мы можем заменить. Что лично я считаю — прекрасно. Чем ниже по лестнице блок — тем меньше придется переписывать. Для того, чтобы реализовать новый контроллер, нужно реализовать его интерфейс и внедрить в текущую систему (каким способом — ваше дело). Хотите поменять текста и цвета? Реализуйте протокол DataSource. Особенно мне нравится идея наличия нескольких PresenterManager. Сейчас вам хочется диалоговое окно, а на другом экране — всплывающий банер сверху (уже в разработке)
Время попсы
Прекрасно понимаю что кол-во звезд слабо коррелирует с качеством кода, надеюсь очевидно что текст совсем не про это.
Пока проект был в работе, я получил так много советов, что потратил больше времени на аргументацию (для себя) почему тот или иной паттерн / идея не подойдет. И это
хорошо, я проделал много работы.
Не считаю себя разработчиком, который может позволить себе давать советы. Но расскажу маленькую историю:
когда я начинал только думать над проектом, мой хороший товарищ Геннадий (имя изменим) работал над одноэкранным приложением. Делал его на VIPER, и особо не вникал почему и зачем его использует. На мои аргументы:
— “
Отпусти проблему, зачем тебе тяжелый паттерн на супер-простом приложении”
он был непреклонен. Прошло несколько месяцев, я релизнул мелкие проекты, убрал в квартире и купил велик, сдал заказчику работу и видел как наступает весна. Геннадий продолжает писать приложение…
Паттерны не таблетка от всех болезней. Не используйте его как показатель профессионализма или потому что "
так делают гуру". Не отсохнут руки, если из MVC сделаете “не эпловский MVC”. Используйте паттерны, когда понимаете что это необходимо.
Используйте их так, чтобы работа становилась проще. Аргумент "
этот паттерн используют крутые проекты" — совсем не имеет отношения целям и врядли такой подход упростит вам жизнь.
Я не призываю отказаться от паттернов, писать все в контроллере и вообще от VIPER отстреливаться магнитной пушкой. Используйте, но обдумано! Ставьте конкретные требования к архитектуре и экспериментируйте — узнавайте что лучше решит поставленные задачи. Если идеально подходит VIPER, но вот
PRESENTER вам кажется лишним — выбросьте его, почему нет? Именно эмпирическая работа с архитектурой даст лучший, пускай и не классический результат.
Я не знаю как называется мой паттерн (компот?) — но я остался доволен тем, как он решает поставленные перед ним задачи. Именно такой результат должно приносить использование паттерна.
— "
Даже профессионалы используют сториборды" — неизвестный автор.
Всем успешных билдов и православного CTR!