История разработки iOS-версии приложения "Айда!"

Айда!
iOS, VIPER, Swift, Apple, Release, Actonica, Ayda, Agile


История о том, как нам нечем было заняться и о том как мы нашли занятие тысячам людей создав приложение для поиска модных событий, спортивных мероприятий, культурных развлечений и вообще любой движухи.

В конце 2015 года, в городе Новокузнецк, в Actonica Studio родилась идея, что было б неплохо создать “рубрикатор” активных развлечений с акциями и скидками. Наш другой продукт “Курсы валют” cash2cash.ru уже исчерпал запас идей. Мы стали задумываться о том, как применить накопленный опыт и создать продукт для широкой аудитории.

Однако, идея ждала около года. В итоге к январю 2017 был разработан примерный дизайн приложения и мы приступили к разработке архитектуры системы и затем самих приложений.

В данной статье речь пойдет о разработке iOS приложения.

Вступительное слово


Компания прошла долгий путь в разработке iOS приложений. Начало было положено порядка 5 лет назад. Тогда это был Objective-C и стандартный подход MVC. Верстка с помощью xib и storyboards. Затем мы пробовали Xamarin (Курсы валют, кстати, написаны с использованием Xamarin + MVVMCross). После выхода второй версии Swift, волею судеб и заказчиков одного из проекта, мы перебрались на Swift (и не жалеем!). Еще во время разработки “Курсы валют” мы отказались от xib и storyboards в пользу верстки из кода. Однажды попробовав всё величие и практически безграничные возможности такого подхода и научившись “мыслить фреймами” остановиться невозможно. Около года назад мы также перешли на AsyncDisplayKit (ныне Texture), разработчики которого составляют ядро Pinterest, Instagram и ранее Facebook. В проекте Айда мы сразу решили верстать на ASDK.


Вопрос архитектуры



Вопрос архитектуры тоже не заставил себя долго ждать. Проект обещал быть большим и необходимо было все сделать максимально гибко, читаемо, “поддерживаемо”. А тут еще команда Rambler выпустили свою книгу The book of VIPER. И понеслось…

Многим известна следующая картинка, которая кратко иллюстрирует VIPER. Здесь показаны основные компоненты по первым буквам. V - View, I - Interactor, P - Presenter, E - Entity, R - Router.


Как это выглядит у нас в проекте? На следующем рисунке представлен скриншот нашего Workspace.


Что есть здесь?
Это глобальная группа Assembly, где находится карта цветов приложения, некоторые идентификаторы, файлы для замены на разные конфигурации сборок.
Группа Shared с ее подгруппами содержит в себе сервисы (микросервисный подход, при котором атомарные единицы бизнес логики выносятся в отдельные классы, н-р, сервис аналитики, сервис геопозиционирования и т.д.), расширения (extensions) и т.д.
Группа API содержит в себе работу с веб-сервисом и маппинги ответов и запросов.
Группа DataManagement содержит в себе DataProvider, который общается с API и локальным хранилищем.
Самое интересное содержится в группе Modules, где и “живет” VIPER, много viper’ов. На следующем рисунке показано содержимое папки Modules.



А если еще глубже? Далее можно увидеть развернутый модуль Feed. Основной экран-лента, где пользователь может видеть все события, предложения и акции в городе.

Feed это обычная таблица с ячейками, футером и хедером. В контексте ASDK у нас есть ASTableNode и ASCellNode, а также ASViewController<ASTableNode>, который “управляет” всем этим безобразием. Команда разработчиков постарались над тем, чтобы работать с Texture (ex-ASDK) было так же легко, как и с UIKit. Однако механизм верстки они создали свой (И надо сказать он очень удобен. Декларативный подход, чем-то похож на верстку в Xaml). Но об этом позднее. Пока вернемся к модулю Feed. Этот модуль у нас переиспользуется для отображения избранного, а также в последующих версиях будет использован для более детального разделения по категориям. Лента поддерживает infinite scroll. Это очень просто реализовать в контексте VIPER. Я не говорю, что это неподъемная задача для MVC или любого другого подхода, если все правильно продумать, однако VIPER дает нам рекомендации, что, где, когда. View отвечает за UI-зависимые действия, говорит presenter’у, что что-то надо сделать. Presenter, будучи главным менеджером (в том смысле, что он координирует работу слоев) модуля, является абстрагированным от UI классом и общается с Interactor и Router, а также говорит View, что нужно сделать в ответ на тот или иной ответ Interactor’а, например.
Так, во View мы слушаем scrollViewWillEndDragging, считаем contentOffset и говорим презентеру, чтоб было б неплохо получить новые элементы, если они есть. Презентер в ответ обращается к интерактору, передавая ему необходимые параметры, а тот, в свою очередь, обращается к dataprovider за данными. Затем идет цепочка ответов и данные, преобразовываясь передаются к слою View, где мы делаем что-то вроде:


в случае, если нам действительно пришла новая страница ленты (на самом деле, там несколько больше условий и все немного сложнее).
“Зачем такая длинная цепочка вызовов?” - задают многие вопрос. Дело  том, что интерактор - это своего рода фасад для внешних сервисов. Естественно, в самом простом случае можно напрямую обратиться к dataprovider и запросить данные. Однако для поддержания чистоты архитектуры надо придерживаться правил. И самому приятнее, и следующий разработчик скажет потом спасибо, а если все методы будут еще и документированы…


Вопросы верстки с Texture

Как я уже говорил выше, у команды Texture (ex-ASDK) свой подход к реализации верстки. У них есть документация с примерами, однако иногда ее бывает мало. Ниже я приведу пример того, как выглядит у нас карточка события в ленте и код ее верстки.



Как видно из рисунков, это динамическая высота ячейки таблицы, картинка, название мероприятия, дата, описание.
А вот код, который делает это возможным:
self.framedContainer.layoutSpecBlock = {
     (node, range) in
     self.imageNode.style.preferredSize = BaseCellConfigurator.coverSize
     self.hasCertificatesNode.style.layoutPosition = CGPoint(x: range.max.width - BaseCellConfigurator.certificatesSize, y: 0)
     let absLayout = ASAbsoluteLayoutSpec(children: [self.imageNode, self.hasCertificatesNode, self.isRecommendedNode])
     self.datesNode.style.minSize = CGSize(width: EventCellConfigurator.datesWidth, height: 20)
     self.datesNode.style.maxSize = CGSize(width: EventCellConfigurator.datesWidth, height: 70)
     let descriptionInsets = UIEdgeInsetsMake(10, 10, 10, 10)
     //max width - insets - interitem spacing - dates width
     let titleMaxHeight : CGFloat = 80
     self.titleNode.style.minSize = CGSize(width: range.max.width - descriptionInsets.left - descriptionInsets.right - EventCellConfigurator.datesWidth - 5, height: 30)
     self.titleNode.style.maxSize = CGSize(width: range.max.width - descriptionInsets.left - descriptionInsets.right - EventCellConfigurator.datesWidth - 5, height: titleMaxHeight)
     //max width - insets
     self.subtitleNode.style.maxSize = CGSize(width: range.max.width - descriptionInsets.left - descriptionInsets.right,
                                              height: range.max.height - descriptionInsets.top - descriptionInsets.bottom - self.imageNode.style.preferredSize.height - titleMaxHeight - 5)
     
     let titleDatesHorizontalStack = ASStackLayoutSpec()
     titleDatesHorizontalStack.direction = .horizontal
     titleDatesHorizontalStack.spacing = 5
     titleDatesHorizontalStack.justifyContent = .spaceBetween
     titleDatesHorizontalStack.alignItems = .start
     titleDatesHorizontalStack.children = [self.titleNode, self.datesNode]
     
     let verticalStack = ASStackLayoutSpec(direction: .vertical,
                                                 spacing: 5,
                                                 justifyContent: .start,
                                                 alignItems: .start,
                                                 children: [titleDatesHorizontalStack, self.subtitleNode])
     let infoInsets = ASInsetLayoutSpec(insets: descriptionInsets, child:verticalStack)
     
     let stack = ASStackLayoutSpec()
     stack.spacing = 0
     stack.direction = .vertical
     stack.alignItems = .stretch
     stack.justifyContent = .start
     stack.children = [absLayout , infoInsets]
     
     return stack
   }


Таким образом, нам предлагается мыслить layout спецификациями. Texture предполагает несколько реализаций типа ASStackLayoutSpec, ASInsetLayoutSpec, ASAbsoluteLayoutSpec. Ниже будут даны ссылки на литературу.

Такой подход сложен тем, что нужно нет мгновенного визуального отображения результата, однако после небольшой практики это не является проблемой.
Как вы могли заметить в коде используется некий EventCellConfigurator. Это структурка, содержащая набор предопределенных размеров (для разных типов экранов и других ситуаций), шрифтов, цветов для текущего view. Такую структуру можно встретить в докладах Yandex также под псевдонимом метрика.


Послесловие



В следующей версии приложения планируется больше фич, а также мы рады расширяться и хотели бы выйти за пределы нашего города. Спасибо за внимание! Если есть предложения по сотрудничеству, вопросы, замечания, пожелания - пишите.

Ссылка на приложение:
http://onelink.to/ayda
Источники:
Вопросы верстки с помощью Texture http://texturegroup.org/docs/layout2-layoutspec-types.html

Comments

Popular posts from this blog

Введение в проблематику архитектуры iOS приложений

VIPER и проблема кодогенерации