Pull to refresh

Как мы стартовали Vivid Money для iOS

Reading time7 min
Views3.2K

Всем привет! Меня зовут Илья. Я - iOS техлид в Vivid Money. Мы больше года занимались разработкой нашего финтех-продукта и теперь готовы поделиться с сообществом приобретенным опытом и знаниями.
Это вступительная статья, в которой я поверхностно затрону несколько технических решений, которые мы сделали на старте, а позже будут опубликованы статьи с детальным разбором самых интересных из них.

Архитектура

Для начала мы определились с архитектурой проекта. Я имею ввиду не только архитектуру экранов/модулей, но и все остальные архитектурные решения. Конечно, рассказать обо всех из них в этом разделе не получится, поэтому затронем только архитектуру модулей, экранов и инъекцию зависимостей.

Архитектура проекта

Так как предполагалось, что проект будет большим и поделенным на продукты, мы решили разделить его на несколько модулей. Это помогает лучше структурировать код, а также облегчает разработку в продуктовых командах. Архитектура модулей выглядит так:

На схеме присутствует 4 слоя:

  • Core. Это проекты, которые либо не связаны с приложением и могут быть использованы где угодно, либо те, от которых зависят все вышестоящие слои.

  • Platform. В этом слое 2 проекта. DesignKit содержит все, что связано с UI приложения: от цветов и шрифтов до готовых компонентов или даже экранов. Platform служит основной для всех фича-проектов и основного приложения. Там содержатся сервисы, общие экраны, конфигурации, сущности и так далее.

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

  • App. Объединяет все проекты воедино.

Архитектура экранов

Мы решили подобрать что-то, что удовлетворяло бы нашим потребностям и не содержало ничего лишнего. В итоге мы пришли к VIP (View, Interactor, Presenter). Мы сохранили основы VIPER, но убрали Router и Entity. Такое разделение модуля помогает лучше его протестировать. Также разделение пригодилось в некоторых местах, где потребовалось использовать разные реализации view или interactor (да, это не миф).

Router мы заменили на Coordinator. Это хороший паттерн, который позволяет сделать модули независимыми друг от друга и сосредоточить всю логику переходов в рамках одной user story внутри одного класса.

Инъекция зависимостей

Мы не используем библиотеку для инъекции зависимостей. На этом хотелось бы закончить, но, кажется, надо дать этому небольшое объяснение.

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

В нашем проекте инъекция зависимостей проста и состоит из двух частей:

  • Класс Container, который содержит все зависимости, объявленные в виде переменных. Этот класс расширяется в каждом проекте и закрывается протоколом. Если, например, нужно получить зависимость из модуля Platform, нужно написать следующий код: let d = (Container.shared() as PlatformContainer).dependency Если не писать код в одной строке, он будет выглядеть чуточку лучше.

  • Класс Assembly, который внедряет зависимости в каждый конкретный модуль (то есть собирает его). Этот класс использует Container для получения зависимостей. Все зависимости в компонентах VIP модуля - это force-unwrapped переменные, значения которым и присваивает Assembly. В остальные классы зависимости внедряются через инициализатор.

Управление сторонними зависимостями

По части управления зависимостями у нас все было как в большинстве проектов – мы использовали CocoaPods. Во-первых, это уже проверенный менеджер зависимостей; во-вторых, его поддерживают почти все open-source библиотеки.

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

На первой итерации мы сделали симбиоз из этих двух менеджеров, так как не все библиотеки поддерживали Carthage. Но через время полностью перешли на Carthage и вдобавок внедрили Rome - утилиту для кэширования собранных библиотек.

Также были попытки использовать SPM, но на тот момент был ряд проблем, которые не позволили это сделать: отсутствие поддержки кастомных конфигураций сборки, проблемы с обновлением библиотеки при изменении ее версии, отсутствие поддержки SPM некоторыми библиотеками. Но мы надеемся на то, что проблемы будут решены, и мы сможем полностью перейти на этот менеджер зависимостей.

Тестирование

В самом начале разработки мы мечтали об отсутствии ручных тестировщиков, что позволило бы нам сделать короткие релизные циклы и избавиться от человеческого фактора при проверке функционала. К сожалению, были обстоятельства, не позволившие добиться этого на старте, но мы уверенно движемся в этом направлении. В любом случае, нам надо было подумать над организацией тестирования продукта. В итоге мы пришли к тому, что будем писать и Unit, и UI тесты.

В Unit тестах мы используем фреймворк SwiftyMocky, который помогает генерировать моки для типов и предоставляет множество полезных функций для тестирования. По уже устоявшейся парадигме мы тестируем, используя структуру Given-When-Then, чтобы все тесты выглядели однотипно и были логически структурированы.

Unit тесты в основном пишутся на общие компоненты (утилиты, сервисы, и т.д.) и классы со сложной бизнес-логикой (в большинстве случаев это Presenter и Interactor), которую будет достаточно проблематично проверить в UI тестах.

UI тесты у нас появились значительно позже. В них мы тоже не стали выдумывать ничего особенного и сделали несколько вспомогательных классов для реализации паттерна Page object.

UI тесты в нашем проекте делятся на 2 типа: компонентные и end-to-end. Компонентные тесты проверяют работу отдельного экрана или его части с использованием моков, а end-to-end тесты проверяют некоторую цепочку экранов и используют реальное API, но на dev контуре.

Также в качестве теста мы внедрили snapshot тесты, но говорить об их пользе пока рано. В идеале хотелось бы использовать их для тестирования компонентов из дизайн системы.

Генерация API клиентов

Наш бэкенд разделен на микросервисы, из чего следует, что и в приложении мы обращаемся к нескольким API. Следить за каждым и обновлять код вручную – слишком трудозатратная задача, и мы решили это автоматизировать.

Каждый микросервис имеет swagger спецификацию, с помощью которой мы генерируем фреймворки, используя Swagger Codegen. Мы немного изменили шаблоны для генерации, чтобы они удовлетворяли нашим требованиям, и автоматизировали процесс обновления фреймворка в CI.

Каждый сгенерированный API клиент находится в отдельном репозитории и добавлен в проект с помощью Carthage.

По части генерации API клиентов есть улучшения, которые можно сделать, но такой подход уже дал огромный прирост к скорости разработки и избавил от необходимости ручного обновления API.

Код-стандарт

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

Самый базовый - это написание code conventions. Там сосредоточены все правила и рекомендации по синтаксису. Почти все из них проверяются с помощью SwiftLint, а для некоторых написаны кастомные правила. Соглашения по стилистике кода помогают нам быстрее проводить код-ревью и делают код проще для чтения.

Далее были описаны:

  • Правила работы с репозиторием: как называть ветки, как писать сообщения к коммитам и так далее;

  • Процесс выполнения задачи: какие задачи можно брать, приоритеты задач, статусы задач, как создать пул-реквест;

  • Используемые паттерны и механизмы: как решать типовые задачи (кэшировать данные, создавать сервисы и тому подобное);

  • Терминология: типичные названия методов или бизнес-определений в коде.

Все это помогает нам разрабатывать одинаково и не заниматься решением проблем, которые уже решены.

Также мы используем Danger CI для проверки пул-реквестов. Список наших правил на данный момент небольшой: проверка на заполнение нужных полей в пул-реквесте, проверка на количество внесенных изменений, поиск TODO, замечания от Swiftlint и пара рекомендательных сообщений. Это помогает не упускать в них важных деталей и напоминать о вещах, о которых можно легко забыть.

Автоматизация

В проекте с большой кодовой базой и растущим числом разработчиков не обойтись без автоматизации. Поэтому верным решением будет уделить этому внимание в самом начале проекта.

Мы написали несколько своих скриптов, которые автоматизируют работу:

  • Скрипт для генерации VIP модулей, который ускоряет разработку экранов.

  • Скрипт для генерации фича-проектов.

  • Скрипты для скачивания локализации, фича-тогглов, удаленной конфигурации и некоторых ресурсов, нужных для приложения. Про этот пункт можно рассказать немного подробнее. Дело в том, что мы скачиваем свежие версии всех этих ресурсов в момент запуска приложения, но на случай проблем с загрузкой мы предусмотрели вариант дефолтной версии ресурсов, которая близка к настоящей. И, чтобы каждый раз не скачивать все эти файлы вручную, мы написали эти скрипты, которые запускаются перед сборкой приложения на CI.

Во избежание merge-конфликтов в файлах проекта мы используем XcodeGen, который генерирует файлы проекта по yaml спецификациям.

С появлением такого количества скриптов встал вопрос удобства использования, ведь запоминать их названия, аргументы и порядок вызова кажется не самой интересной задачей. Поэтому мы создали скрипт, который вызывает остальные скрипты. Цель его работы проста - привести проект в актуальное состояние. В процессе работы он скачивает ресурсы, обновляет зависимости, генерирует файлы проекта и моки, настраивает схемы. Сначала мы не вызывали этот скрипт руками, а сделали его вызов автоматическим на каждый merge, pull или checkout. Но так как скрипт выполнялся некоторое время, это приводило к постоянным ожиданиям, хотя после, например, checkout в новую ветку ничего в проекте не поменялось. Поэтому на данный момент он вызывается вручную, но если бы получилось значительно сократить время его работы, то мы бы вернулись к автоматическому вызову.

Также мы создали приложение для Mac OS, которое предоставляет интерфейс для вызова всех наших скриптов. Оно было очень простым и мало востребованным, но в данный момент мы его перерабатываем, чтобы упростить работу с проектом.

Настройка проекта

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

Сначала это был исполняемый файл, который надо было запустить, чтобы скачались все зависимости (ruby, brew, python, и так далее) и выполнились необходимые настройки. Но позже мы реализовали настройку проекта через Ansible. Это позволило нам держать все в одном месте и настраивать не только компьютеры сотрудников, но и билд-агенты.

Подводя итоги

Мы рассказали коротко о тех вещах и том опыте, который мы накопили, когда только начали делать первые шаги в проекте.

Мы планируем и дальше делиться своим опытом разработки, так как считаем это важной частью развития комьюнити. Чтобы мы делали это лучше, пишите комментарии и конструктивную критику. Будем очень признательны.

Всем спасибо!

Tags:
Hubs:
Total votes 5: ↑5 and ↓0+5
Comments16

Articles

Information

Website
vivid.money
Registered
Founded
Employees
201–500 employees
Location
Германия