Сказка о переходе от монолитной архитектуры к пакетной.

Glose была приобретена Medium в январе прошлого года, и с тех пор команда Glose была занята интеграцией со всеми нашими новыми коллегами и созданием новых блестящих функций.

Глос все еще здесь. Наша команда, базирующаяся в Париже (а также в Украине и Великобритании), теперь является частью одной группы, работающей над достижением единой цели по набору функций, которые еще предстоит раскрыть.

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



Со мной и Аллой Дубовской мы успешно присоединились к команде iOS, работающей над приложением Medium. Я хотел бы поблагодарить Алейну Кафкес за потрясающий опыт адаптации!

Даже если я еще не могу говорить об этой функции, я могу рассказать о ее технических деталях. База кода iOS представляет собой смесь устаревшего и современного кода с очень гибкой архитектурой. У нас все еще есть код Objective-C, который поддерживает некоторые основы и пользовательский интерфейс. Даже если сейчас весь код написан на Swift, и большинство функций, расположенных на лицевой стороне приложения, были переделаны с нуля.

Поскольку Apple собирается выпустить iOS 15, а большая часть нашей пользовательской базы работает на iOS 14 и iOS 13, мы почувствовали, что настало подходящее время для создания нашей новой функции с использованием SwiftUI только для iOS 13+.

Идея состоит в том, что SwiftUI позволит нам быстрее создавать пользовательский интерфейс и упростить итерацию с дизайном. И пока все это подтверждено! И пока мы здесь, мы полностью приняли Combine. И здесь я должен поблагодарить Логана Мозли за его постоянный еженедельный семинар по Combine!

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

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

И я хочу отметить, что первыми инженерами, которые внедрили SwiftUI в нашу кодовую базу, были Майк МакГи, Майкл Барретт и Джозеф Бехар! Это было ступенькой в ​​нашей современной архитектуре. Некоторые из вас, возможно, уже используют версию SwiftUI для представлений ответов / ответов.

Архитектура

Как я уже говорил ранее, проект iOS очень подвижен, и в настоящее время предпринимаются попытки подготовить архитектуру к будущему, основанную на SwiftUI, пакетах Swift и Combine.

Именно этим мы и занимаемся в последнее время. Мы переместили наши сгенерированные модели GraphQL / Apollo на уровне приложения в пакет ApolloModel Swift. А благодаря предыдущей итерации Майка МакГи, посвященной различным движущимся частям в наших внутренних системах iOS, было довольно легко собрать все это вместе и подготовить для Combine.

Для этого нам сначала пришлось переместить некоторые зависимости Cocoapods в Swift Package. Именно так мы и поступили с нашим GraphQL-клиентом Apollo-iOS. Мы также переместили несколько других модулей в Swift Packages. Мы пока не можем сделать это для всего, потому что у нас все еще есть код Objective-C в зависимости от некоторых модулей.

Это позволило нам создать гораздо больше автономных пакетов функций. Мы также сделали SharedUI, где у нас есть много автономных компонентов UIKit и SwiftUI. Кроме того, у нас есть различные пакеты функций, например, тот, над которым мы работаем для нашей новой функции.

Мы тоже хотим пойти дальше. Логан Мозли работает над архитектурной документацией. Идея состоит в том, чтобы создать пакет MediumModel, чтобы переместить в него наши данные и модель просмотра. Идея состоит в том, что вместо пакетов функций у нас есть пакеты чистого пользовательского интерфейса. Это позволит нам ускорить сборку и время предварительного просмотра SwiftUI, поскольку нам не нужно компилировать всю базу кода приложения для предварительного просмотра пользовательского интерфейса новой функции. Мы просто создаем пакет функций, над которым работаем, чтобы мгновенно получать предварительные версии SwiftUI. Это огромный прирост производительности!

Мы также хотим иметь пакеты LiveService и FakeService, но об этом чуть позже.

Вот небольшой отредактированный совместимый снимок экрана нашего текущего дерева исходных текстов Xcode:

Уровень обслуживания

Говоря об услугах, мы пробовали что-то новое с функциями Combine только для iOS 13. В Medium у нас много тестов, и мы очень серьезно к ним относимся. Поэтому нам нужен хороший код и разделение ответственности. Наши стандартные службы регистрируются при запуске приложения, и мы можем получить их и внедрить в любое место приложения.

А в тестовой среде мы регистрируем и внедряем их макетную версию везде, где это необходимо.

Но для нашего нового кода SwiftUI мы теперь пишем их немного по-другому, и нам не нужно регистрировать их в нашем глобальном реестре сервисов. Мы определяем прототип, который выглядит так:

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

Затем у нас есть две их реализации в зависимости от того, является ли это действующей версией службы в нашем пакете LiveService:

И поддельная версия сервиса в нашем FakeService для тестирования:

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

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

А если вам нужно протестировать это, вы проходите реализацию .fake, и все готово.

Мы черпали вдохновение из этого видео pointfree.co о зависимостях. Он очень хорошо объясняет концепцию модульности и разделенных сервисов. Как и они, в нашем случае мы предпочли структуру протоколам.

У нас также есть некоторые другие сервисы, которые не обрабатывают сетевую информацию и выглядят немного более стандартными. Они все еще зарегистрированы в нашем реестре услуг:

Слой пользовательского интерфейса

Пользовательский интерфейс для нашей новой функции в основном выполнен в собственном Swift Package. Это гарантирует, что мы сможем собрать этот пакет полностью отдельно от приложения. У нас быстрое время сборки и поэтому очень быстрый предварительный просмотр SwiftUI. Это то, что позволяет нам так быстро выполнять итерацию в пользовательском интерфейсе. Нам не нужно запускать приложение для тестирования всех наших настроек.

Все наши представления также позволяют легко имитировать модели представлений или упрощенные модели GraphQL / Fragment. Обычно мы определяем фрагмент так:

Составьте протокол, упрощающий чтение:

А затем расширите фрагмент нашим протоколом и реализуйте все, что нам нужно.

Таким образом, нам не нужно передавать конкретный тип GQL, мы можем просто передать все, что соответствует протоколу out, не имеет значения, поддерживается ли это типом, сгенерированным GraphQL, или каким-либо объектом, который мы определили для предварительного просмотра или тестирования.

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

Конечно, это не подходит для всех наших сценариев использования. У нас также есть концепция магазинов, которая, вероятно, не подходит для такого рода объектов. Эти хранилища опубликовали свойства и обрабатывают связь между хост-контроллером и корневыми представлениями SwiftUI. У нас есть различные опубликованные свойства, которые гарантируют, что представление всегда находится в самом актуальном состоянии.

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

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

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

Итак, мы можем инициализировать наше представление SwiftUI в нашем UIHostingController и подключить наши обработчики действий следующим образом:

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

Мы также решили, что часть навигации может остаться в SwiftUI. Если это компонент или экран, в которых нам не нужно перемещаться за пределами этого конкретного потока, тогда нам очень удобно использовать .sheet, он работает очень хорошо.

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

Мы также широко используем новый API Меню для Button. Алла Дубовская написала гениальный компонент, который может отображать меню или лист при нажатии кнопки в зависимости от версии iOS с использованием того же интерфейса. Жаль, что меню SwiftUI все еще не поддерживает деструктивные действия… возможно, для iOS 15

Еще одна проблема связана со списком. Мы используем его в iOS 13, даже если он не полностью соответствует UI / UX. Мы нашли несколько надежных способов снятия разделителя. И мы используем LazyVStack вместо List в iOS 14. Он не вызывает каких-либо нестандартных нежелательных действий, но в некоторых случаях имеет некоторые проблемы с производительностью. Это так волшебно со SwiftUI, потому что это просто замена компонентов верхнего уровня / представления контейнера в зависимости от версии ОС, и все. Например, нам не нужно переписывать UITableViewCell для некоторых других представлений.

У нас есть очень простая реализация панели навигации с постепенным исчезновением заголовка в зависимости от смещения прокрутки. Если бы я не сказал вам, что это в SwiftUI, вы бы никогда не узнали:

Я не могу сказать достаточно, SwiftUI позволяет вам сделать ваш пользовательский интерфейс намного быстрее, когда вы освоитесь с ним. Вначале у нас была медленная пара недель, когда мы адаптируем все наши инструменты к SwiftUI. Нам пришлось соединить систему дизайна (которая также есть в ее пакете) и некоторые другие важные компоненты, чтобы достичь максимальной скорости.

Несколько заключительных заметок

Я был очень скептически настроен, прежде чем использовать SwiftUI в кодовой базе, которая в значительной степени полагалась на Objective-C и потребовала от нас двух функций, чтобы сделать это правильно. Но теперь, когда у нас есть правильная архитектура, я очень счастлив. До использования SwiftUI в приложении Medium я использовал его только в чистых проектах SwiftUI, я отправил свое приложение с полностью открытым исходным кодом SwiftUI в App Store, и это был прекрасный опыт. Но до сих пор я всегда стеснялся вводить его в существующую кодовую базу, а не на SwiftUI.

Не будь таким, как я, не стесняйся!

SwiftUI - это будущее, и оно останется здесь надолго.

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

Спасибо за чтение 🚀