Придя в компанию МегаФон как iOS-разработчик, Валентин Чернов попал в сегодняшний основной тренд — переход приложений в оффлайн. Основная работа Валентина — разработка главного приложения МегаФона, мобильного личного кабинета. Это в нем вы можете смотреть баланс и вообще управлять им, подключать/отключать услуги и сервисы, смотреть сторисы, участвовать в конкурсах и видеть персональные предложения от партнеров МегаФона.

Даже несмотря на то, что оно недавно получило две премии Рунета, с таким количеством сервисов приложение, конечно, будет тяжелым и весомым. И перевод его в оффлайн — большая длинная задача. Сейчас МегаФон выбрал возможность работать при нестабильной связи как одну из важных точек роста. Хотя уже двадцать первый век, 4G-5G сети наступают по всем фронтам, тем не менее для приложений федерального масштаба в России найдутся места, где связь временно отключается или пропадает надолго. И нужно, чтобы даже в этом случае приложение работало без сбоев.

О том, как эта задача выполнялась в течение последних 5 месяцев, как выбирали и воплощали архитектуру проекта, какие технологии выбрали и использовали, а также чего достигли и что запланировали на будущее, Валентин рассказал в докладе на Конференции разработчиков мобильных приложений Apps Live 2020.



Задача


Бизнес сказал — идём в оффлайн, чтобы пользователь мог успешно взаимодействовать с приложением в условиях нестабильного сетевого подключения. Мы, как команда разработки, должны были гарантировать Offline first — работу приложения даже при нестабильном или совсем отсутствующем интернете. Сегодня расскажу о том, с чего мы начали и какие первые шаги в этом направлении сделали.

Стек технологий


Помимо стандартной архитектуры MVC мы используем:

Swift + Objective-C


Большая часть кода (80% нашего проекта) написан на Objective-C. А уже новый код мы пишем на Swift.

Модульная архитектура


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

Submodules (библиотеки)


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

Core Data для локального хранения информации


При выборе для нас главным критерием были нативность и интеграция с iOS фреймворками. А эти преимущества Core Data стали решающими при его выборе:

  • Автосохранение стека и данных, которые получаем;
  • Удобная работа с моделью данных, достаточно удобная работа с графическим редактором для составления сущностей (и возможности их править, передавать новым разработчикам и т.д.)
  • Поддержка миграции и версионирования;
  • Ленивая загрузка объектов;
  • Работа в многопоточном режиме;
  • Отслеживание изменений;
  • Интеграция с UI (FRC);
  • Работа с запросами в БД на более высоком уровне (NSPredicates).

UIManaged document


У UI kit есть встроенный класс, называемый UIManagedDocument, и который является подклассом UIDocument. Его основное отличие — при инициализации управляемого документа указывается URL-адрес для расположения документа в локальном или удаленном хранилище. Затем объект документа полностью создает стек Core Data прямо из коробки, который используется для доступа к постоянному хранилищу документа с использованием объектной модели (.xcdatamodeld) из основного пакета приложения. Это удобно и имеет смысл, даже несмотря на то, что мы живем уже в 21 веке:

  • UIDocument автосохраняет текущее состояние сам, с определенной частотой. Для особо критичных секций мы можем вручную вызывать сохранение.
  • Можно отслеживать состояния документа. Если документ открыт для работы или находится в каких-то конфликтных ситуациях — например, мы осуществляем сохранение из разных точек, и где-то вдруг мы вызвали конфликт, — мы можем это отследить, обработать, поправить и уведомить пользователя правильной понятной ошибкой.
  • UIDocument позволяет читать и записывать документ асинхронно.
  • Он может создать стек Core data из коробки.
  • Есть встроенная функция хранения в iCloud и синхронизации с облаком. Это как раз то, к чему мы в будущем стремимся.
  • Поддержка версионности.
  • Используется Document based app парадигма — представление модели данных как контейнер для хранения этих данных. Если посмотреть на классическую модель MVC в документации Apple, можем увидеть, что Core data создана как раз для того, чтобы управлять этой моделью и помогать нам на более высоком уровне абстракции работать с данными. На уровне модели работаем, подключая UIManagedDocument со всем созданным стеком. А сам документ рассматриваем как контейнер, который хранит Core data и все данные из кэша (от экранов, пользователей). Плюс это могут быть картинки, видео, тексты — любая информация.
  • Мы же рассматриваем наше приложение, его запуск, авторизацию пользователей и все его данные как некий большой документ (файл), в котором хранится история нашего пользователя. И которую мы можем передавать, очищать и владеть ею на уровне приложения:



Процесс


Как мы проектировали архитектуру


Процесс проектирования у нас проходит в несколько этапов:

  1. Анализ технического задания.
  2. Отрисовка диаграммы UML-диаграмм. Мы используем в основном три типа UML-диаграмм: class diagram (диаграмма классов), flow chart (блок-схема), sequence diagram (диаграмма последовательностей). Это прямая обязанность senior-разрабочиков, но могут делать и разработчики с меньшим опытом. Это даже приветствуется, так как позволяет хорошо погрузиться в задачу и изучить все ее тонкости. Что помогает найти в ТЗ какие-то недоработки, а также структурировать всю информацию по задаче. И мы стараемся учитывать кросс-платформенность нашего приложения — мы тесно работаем с Android-командой, рисуя одну схему на две платформы и стараясь использовать основные общепринятые паттерны проектирования от «банды четырёх».
  3. Ревью архитектуры. Как правило, ревью и оценку проводит коллега из смежной команды.
  4. Реализация и тестирование на примере одного UI модуля.
  5. Масштабирование. Если тестирование проходит успешно, мы масштабируем архитектуру на все приложение.
  6. Рефакторинг. Чтобы проверить, не упустили ли мы что-нибудь.

Сейчас, после 5 месяцев разработки этого проекта, я могу показать весь наш процесс в три этапа: что было, как менялось и что получилось в результате.

Что было


Нашей отправной точкой была стандартная MVC архитектура — это связанные между собой слои:

  • UI слой, полностью программно сверстанный с использованием Objective C;
  • Класс презентации (модель);
  • Сервисный слой, где мы работаем с сетью.

Activity indicator был расположен в том месте схемы, где мы держим пользователя в режиме ожидания — он хочет быстрого результата, но вынужден смотреть на какие-то лоадеры, индикаторы и прочие сигналы. Это было нашим слабым местом в user experience:



Переходный период


В переходный период мы должны были внедрить кэширование для экранов. Но так как приложение большое и содержит много legacy кода на Objective C, мы не можем просто взять и удалить все сервисы и модели, вставив Swift-код — мы должны учитывать, что параллельно с кэшированием у нас в разработке еще много других продуктовых задач.

Мы нашли безболезненный способ, как интегрироваться в текущий код максимально эффективно, ничего при этом не сломав, а первую итерацию провести максимально мягко. В левой части предыдущей схемы мы полностью убрали все, что связано с сетевыми запросами — по интерфейсу сервис теперь общается с DataSourceFacade. И сейчас это — фасад, с которым работает сервис. Он ждет от DataSource те данные, которые раньше получал от сети. А в самом DataSource скрыта логика по добыче этих данных.

В правой части схемы мы разбили получение данных на команды — паттерн Command нацелен выполнить какую-то базовую команду и получить результат. В случае iOS мы используем наследников NSOperation:



Каждая команда, которую вы здесь видите — это операция, в которой есть логическая единица ожидаемого действия. Это получение данных из БД (или сети) и сохранение этих данных в Core data. Или, например, главная задача AcquireCommand — это не только вернуть фасаду источник данных, но и дать нам возможность разрабатывать код таким образом, чтобы получать данные через фасад. То есть взаимодействие с операциями идет через данный фасад.

А основная задача операций — передать данные DataSource для DataSourceFacade. Конечно, мы выстраиваем логику так, чтобы как можно быстрее показать данные пользователю. Как правило, внутри DataSourceFacade у нас есть операционная очередь, где мы запускаем наши NSOperations. В зависимости от настроенных условий мы можем принять решение, когда показывать данные из кэша, а когда — получать из сети. При первом запросе источника данных в фасаде мы идем в БД Core data, достаем оттуда через FetchCommand данные (если они там есть) и моментально возвращаем их пользователю.

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

Как стало


К такой более лаконичной схеме мы идем (и придем в итоге):



Сейчас из этого у нас есть:

  • UI слой,
  • фасад, через который мы предоставляем наш DataSource,
  • команда, которая этот DataSource вместе с updates возвращает.

Что такое DataSource и почему мы о нем так много говорим


DataSource — это объект, который предоставляет данные для слоя презентации и соответствует заранее определенному протоколу. А протокол должен быть подстроен под наш UI и предоставлять данные для нашего UI (неважно, для конкретного экрана или для группы экранов).

У DataSource, как правило, две основных обязанности:

  1. Предоставление данных для отображения в UI слое;
  2. Уведомление UI слоя об изменениях в данных и досылка необходимой пачки изменений для экрана, когда мы получаем уже обновление.

Мы у себя используем несколько вариантов DataSource, потому что у нас много Objective C legacy кода — то есть, мы не везде можем легко наш Swift’овый DataSource воткнуть. Еще мы пока не везде используем коллекции, но в будущем перепишем код именно для использования CollectionView экранов.

Пример одного из наших DataSource:



Это DataSource для коллекции (он так и называется CollectionDataSource) и это достаточно несложный класс с точки зрения интерфейса. Он принимает в себя коллекцию, настроенный fetchedResultsController и CellDequeueBlock. Где CellDequeueBlock — type alias, в котором мы описываем стратегию по созданию ячеек.

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



FetchedResultsController — сердце нашего DataSource. В документации Apple вы найдете много информации по работе с ним. Как правило, мы получаем все данные с его помощью — и новые данные, и данные, которые были обновлены или удалены. При этом мы параллельно запрашиваем данные из сети. Как только данные были получены и сохранились в БД, мы получили update у DataSource, и update пришел к нам в UI. То есть одним запросом мы и получаем данные, и показываем их в разных местах — классно, удобно, нативно!

И везде, где можно использовать уже готовые DataSource с таблицами или с коллекциями, мы это делаем:



В тех местах, где у нас много экранов и не используются таблицы и коллекции (а используется Objective C программная верстка), мы оцениваем, какие данные нам нужны для экрана, и через протокол описываем наш DataSource. После этого пишем фасад — как правило, это тоже публичный протокол Objective C, через который мы запрашиваем наш DataSource. А дальше уже идет вход в Swift’овый код.

Как только мы будем готовы перевести экран полностью в Swift-реализацию, достаточно будет убрать Objective C-обертку — и можно работать напрямую со Swift’овым протоколом, благодаря кастомному DataSource.

Сейчас мы используем три основных варианта DataSources:

  • TableViewDatasource + cell strategy (стратегия по созданию ячеек);
    CollectionViewDatasource + cell strategy (вариант с коллекциями);
    CustomDataSource — кастомный вариант, его мы сейчас используем больше всего.

    Результаты


    После всех шагов по проектированию, реализации и взаимодействию с legacy кодом бизнес получил:

    • Существенно повысилась скорость доставки данных до пользователя за счет кэширования — это, наверное, очевидный и логичный результат.
    • Мы теперь на шаг ближе к парадигме offline first.
    • Настроили процессы архитектурного кроссплатформенного ревью внутри iOS & Android команд — все причастные к этому проекту разработчики владеют информацией и легко обмениваются опытом между командами.
    • Как бонус получили хорошую документацию к проекту за счет схем и описаний. Мы ее показываем нашим новым разработчикам, чтобы им было проще понять, как у нас проброшен мостик между legacy и новым кодом, и как работает сам процесс кэширования.
    • Мы уложились в сжатые спортивные сроки, и это — на живом проекте. У нас получилось, условно говоря, провести ремонт, никого не выселяя из офиса, но все продолжали работать, и даже не дышали строительной пылью.

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

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

    Ссылки


    • Document-based programming guide. Это довольно старый документ, Apple его уже не рекомендует использовать. Но я бы порекомендовал посмотреть хотя бы для дополнительного развития. Там очень много полезной информации.
    • Document-based WWDC: первый и второй
    • DataSources