Русский
Русский
English
Статистика
Реклама

Modular

SPM модуляризация проекта для увеличения скорости сборки

11.11.2020 12:15:25 | Автор: admin
Привет, Хабр! Меня зовут Эрик Басаргин, я iOS-разработчик в Surf.

На одном большом проекте мы столкнулись с низкой скоростью сборки от трёх минут и более. Обычно в таких случаях студии практикуют модуляризацию проектов, чтобы не работать с огромными монолитами. Мы в Surf решили поэкспериментировать и модуляризовать проект с помощью Swift Package Manager менеджера зависимостей от Apple.

О результатах расскажем в другой статье, а сейчас ответим на главные вопросы: зачем это всё нужно, почему мы выбрали SPM и как делали первые шаги.



Почему именно SPM


Ответ прост это нативно и ново. Он не создает overhead в виде xcworkspace, как Cocoapods, к примеру. К тому же SPM open-source проект, который активно развивается. Apple и сообщество исправляют в нем баги, устраняют уязвимости, обновляют вслед за Swift.

Делает ли это сборку проекта быстрее


Теоретически сборка ускорится из-за самого факта разделения приложения на модули framework'и. Это значит, что каждый модуль будет собираться только в том случае, когда в него внесли изменения. Но утверждать точно можно будет только по окончанию эксперимента.

Note: Эффективность модуляризации напрямую зависит от правильного разбиения проекта на модули.

Как сделать эффективное разбиение


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

Разделять обобщённую функциональность. Каждый модуль отвечает за какую-то категорию, к примеру:

  • CommonAssets набор ваших Assets'ов и public интерфейс для доступа к ним. Обычно он генерируется с помощью SwiftGen.
  • CommonExtensions набор расширений, к примеру Foundation, UIKit, дополнительные зависимости.

Разделять flow'ы приложения. Рассмотрим древовидную структуру, где MainFlow главное flow приложения. Представим, что у нас новостное приложение.

  • NewFlow экраны новостей и обзора конкретной новости.
  • FavoritesFlow экран со списком избранных новостей и экран обзора конкретной новости с дополнительным функционалом.
  • SettingsFlow экраны настроек приложения, аккаунта, категорий и т. д.

Выносить reusable компоненты в отдельные модули:

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

Когда нужно выносить компонент в отдельный модуль


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

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

Создаём проект с использованием SPM


Рассмотрим создание тривиального тестового проекта. Я использую Multiplatform App project на SwiftUI. Платформа и интерфейс тут не имеют значения.

Note: Чтобы быстро создать Multiplatform App, нужен XCode 12.2 beta.

Создаём проект и видим следующее:



Теперь создадим первый модуль Common:

  • добавляем папку Frameworks без создания директории;
  • создаём SPM-пакет Common.



  • Добавляем поддерживаемые платформы в файл Package.swift. У нас это platforms: [.iOS(.v14), .macOS(.v10_15)]



  • Теперь добавляем наш модуль в каждый таргет. У нас это SPMExampleProject для iOS и SPMExampleProject для macOS.



Note: Достаточно подключать к таргетам только корневые модули. Они не добавлены как подмодули.

Подключение завершено. Теперь достаточно настроить модуль с public интерфейсом и вуаля, первый модуль готов.

Как подключить зависимость у локального SPM-пакета


Добавим пакет AdditionalInfo как Common, но без добавления к таргетам. Теперь изменим Package.swift у Common пакета.



Добавлять больше ничего не нужно. Можно использовать.

Пример, приближенный к реальности


Подключим к нашему тестовому проекту SwiftGen и добавим модуль Palette он будет отвечать за доступ к палитре цветов, утвержденной дизайнером.

  1. Создаем новый корневой модуль по инструкции выше.
  2. Добавляем к нему корневые каталоги Scripts и Templates.
  3. Добавляем в корень модуля файл Palette.xcassets и пропишем какие-либо color set'ы.
  4. Добавляем пустой файл Palette.swift в Sources/Palette.
  5. Добавим в папку Templates шаблон palette.stencil.
  6. Теперь нужно прописать конфигурационный файл для SwiftGen. Для этого добавим файл swiftgen.yml в папку Scripts и пропишем в нем следующее:

xcassets:  inputs:    - ${SRCROOT}/Palette/Sources/Palette/Palette.xcassets  outputs:    - templatePath: ${SRCROOT}/Palette/Templates/palette.stencil      params:        bundle: .module        publicAccess: true      output: ${SRCROOT}/Palette/Sources/Palette/Palette.swift


Итоговый внешний вид модуля Palette

Модуль Palette мы с вами настроили. Теперь надо настроить запуск SwiftGen, чтобы палитра генерировалась при старте сборки. Для этого заходим в конфигурацию каждого таргета и создаем новую Build Phase назовём ее Palette generator. Не забудьте перенести эту Build Phase на максимально высокую позицию.

Теперь прописываем вызов для SwiftGen:

cd ${SRCROOT}/Palette/usr/bin/xcrun --sdk macosx swift run -c release swiftgen config run --config ./Scripts/swiftgen.yml

Note: /usr/bin/xcrun --sdk macosx очень важный префикс. Без него при сборке вылетит ошибка: unable to load standard library for target 'x86_64-apple-macosx10.15.


Пример вызова для SwiftGen

Готово доступ к цветам можно получить следующим образом:
Palette.myGreen (Color type in SwiftUI) и PaletteCore.myGreen (UIColor/NSColor).

Подводные камни


Перечислю то, с чем мы успели столкнуться.

  • Всплывают ошибки архитектуры и портят всю логику разбиения на модули.
  • SwiftLint & SwiftGen не уживаются вместе при подгрузке их через SPM. Причина в разных версиях yml.
  • В крупных проектах не получится сразу избавиться от Cocoapods. А разбивать уже созданный проект с закреплёнными версиями подов настоящее испытание, потому что SPM только развивается и не везде поддерживается. Но SPM и Cocoapods более-менее работают параллельно: разве что поды могут кидать ошибку MergeSwiftModule failed with a nonzero exit code. Это происходит довольно редко, а решается очисткой и пересборкой проекта.
  • На данный момент SPM не позволяет прописать пути поиска библиотек. Приходится явно указывать их с завязкой на -L$(BUILD_DIR).

SPM замена Bundler?


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

SPM дает возможность вызывать swift run, если добавить Package.swift в корень вашего проекта. Что это нам дает? К примеру, можно вызвать fastlane или swiftlint. Пример вызова:

swift run swiftlint --autocorrect.
Подробнее..

Разделяй и властвуй Navigation Component в многомодульном проекте

22.01.2021 00:08:40 | Автор: admin

В этой статье вы узнаете, как можно организовать графы отдельных модулей / фич / user story, централизовать их, построить прямую навигацию между ними и присыпать сверху Safe Args плагином.

Вы сейчас в третьей части большого материала про Navigation Component в многомодульном проекте. Если не поняли ни единого слова выше, то призываю сначала ознакомиться с тем:

  • Что за зверь этот Navigation Component.

  • Как работает плагин Safe Args и что он делает.

Ну а если вы уже знакомы с этой библиотекой, то для вас есть приятный бонус в следующей статье подход к организации iOS-like multistack-навигации.

Сначала посмотрим, как выглядит разбиение проекта на модули у нас в компании, в которой я работаю (magora-systems.com):

  1. :app основной модуль и точка входа в приложение. Он должен знать обо всех модулях, участвующих в приложении.

  2. :core-модуль содержит в себе все базовые вещи: базовые классы, модели, entity, DTO, extension-ы и пр.

  3. Утилитарные модули служат для инкапсуляции функционала основных компонентов приложения. Например, работа с сетью, БД или той же навигацией.

  4. Feature-модули заключают в себе работу определенной фичи / user story, будь то флоу или экран.

Что ж, давайте натянем сову на глобус сделаем навигацию с подключенным Safe Args плагином.

Если пользоваться одним единственным графом, то будет так:

Выглядит запутанно и неочевидно. Чтобы не допустить подобного, нужно совершить ряд действий:

  1. Организовать графы для каждого feature-модуля.

  2. Выделить отдельный Top-level граф.

  3. Сделать удобные переходы между графами модулей.

  4. Решить, где хранить Top-level граф.

Теперь подробнее о каждом.

Организовать графы для каждого feature-модуля

Чтобы не было проблем с навигацией между destination-ами внутри одного модуля, сделаем отдельные графы под каждый feature-модуль и там обозначим все конечные точки и связи между ними. При таком подходе навигация фичи полностью инкапсулируется внутри модуля, а Safe Args классы, принадлежащие соответствующему графу, генерируются только тут.

Выделить отдельный Top-level граф

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

Выглядит не так эффектно, зато эффективно.

Сделать удобные переходы между графами модулей

А для того, чтобы можно было навигироваться между графами различных фич, нужно подвести к каждому global action.

Решить, где хранить Top-level граф

Тут есть несколько вариантов, у каждого есть свои плюсы и минусы:

  1. Базовый модуль (:core)

О нем знает большинство модулей, но не все. Он не знает ни об одном модуле, поэтому не сможет увидеть графы. Стоит заметить, что приложение скомпилится и будет работать несмотря на ошибки Lint-a, при мерже ресурсов все равно всё сливается воедино.

При изменении графа надо будет пересобирать проект, а так как от него зависит много модулей, то их тоже надо будет пересобрать.

  1. Главный модуль (:app)

+ Знает об абсолютно всех модулях.

О нем не знает ни один модуль.

Safe args сгенерирует global action-ы в недоступном для feature-модулей месте, поэтому мы не сможем ходить между графами.

Результаты не то чтобы очень радужные, но у нас есть целый один плюс в пользу :app-модуля, так что оставим Top-level граф в нем и займемся минусами этого подхода.Для этого нам понадобится

Минус: о нем не знает ни один модуль

Решение: сделать отдельный модуль (:navigation), о котором будут знать абсолютно все модули, которые будут хоть как-то взаимодействовать с навигацией.

Добавить в него все id глобальных action-ов. Таким образом generated-файлы поймут, с чем работают, и будут иметь доступ к id каждого глобального перехода.

<item name="actionglobalnavsignin" type="id"/><item name="actionglobalnavsignup" type="id"/><item name="actionglobalnavhome" type="id"/><item name="actionglobalnavuserslist" type="id"/><item name="actionglobalnavuserdetails" type="id"/><item name="actionglobalnavonC11CglobalC12Csettings" type="id"/><item name="actionC13Cto_faq" type="id"/>

Минус: сгенерированные Directions и Args лежат в :app модуле

Safe args сгенерирует global action-ы в недоступном для feature-модулей месте, поэтому мы не сможем ходить между графами.

Решение: перенести и доработать generated-файлы. Тут немного сложнее и придется запачкать руки о билд-скрипты. Generated-классы находятся в build-папке того модуля, где находится граф (сейчас это :app), а использовать его в :navigation-модуле неудобно. Поэтому воспользуемся костылем небольшой хитростью: во время билда дождемся конца работы таски generateSafeArgs, перекинем все созданные файлы в модуль навигации и, так как Args- и Directions-классы используют R файл модуля :app, добавим импорт нашего модуля навигации.

ext {navigationArgsPath = '/build/generated/source/navigation-args'appNavigation = "${project(':app).projectDir.path}$navigationArgsPath"navigationPath = "${project(':navigation').projectDir.path}$navigationArgsPath"navigationPackage = com.example.navigation}tasks.whenTaskAdded { task ->if (task.name.contains('generateSafeArgs')) {task.doLast {fileTree(appNavigation).filter { it.isFile() && it.name.contains("Directions") }.forEach { file ->if (file.exists()) {def lines = file.readLines()lines = lines.plus(2, "import $navigationPackage.R")file.text = lines.join("\n")}}}move(file("$appNavigation"), file("$navigationPath"))}}

В итоге

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

Помимо всего прочего мы решили проблему с созданными плагином файлами теперь ребилд в разные флейворы и типы не будет нам докучать, как бы это было с простым добавлением sourceSet-s в модуль навигации.

На этой победе я останавливаться не стал и решил посмотреть, что еще можно с этим сделать. Для этого как никогда кстати подошел проект, в котором заказчик хотел приложение с нижним меню и чтобы каждая вкладка сохраняла свое состояние при уходе с неё. Именно о таком решении финальная часть моей истории про iOS-like multistack-навигацию.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru