Pull to refresh
86.89
hh.ru
HR Digital

Tuist: Xcode-проект на стероидах

Reading time9 min
Views19K


Стандартное представление Xcode-проекта сложно назвать комфортным для командной работы. Даже в небольших проектах часто возникают merge-конфликты после изменения состава исходников в разных ветках.


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


Да, ограничения Xcode можно победить, но решением в основном является "винегрет" из сторонних инструментов, заправленный собственными Shell или Ruby скриптами, в которых мало кто разбирается.


Но есть куда более изящное и комплексное решение — Tuist. С ним мы и познакомимся в этой статье.


Что такое Tuist?


Tuist — это open source инструмент для работы со структурой проекта.


Его основная функция — генерация Xcode-проектов из специальных файлов с описанием проекта в удобном формате. В терминологии Tuist эти файлы называются манифестами.


Как и другие подобные инструменты (XcodeGen, Struct и Xcake), Tuist синхронизирует структуру Xcode-проекта с иерархией файлов на диске. Это отлично решает проблему merge-конфликтов в файлах project.pbxproj, так как их можно вообще удалить из репозитория и добавить в .gitignore.


От аналогов Tuist выгодно отличается способом описания проекта — всё на Swift, как и положено, с автодополнением и прочими благами IDE. Ещё одно важное преимущество Tuist — кастомные хелперы, которые позволяют расширять его возможности и переиспользовать код в манифестах.


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


Как установить?


Для установки Tuist достаточно выполнить команду в терминале:


bash <(curl -Ls https://install.tuist.io)

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


Интеграция


Для демонстрации возможностей Tuist воспользуемся специально подготовленным стартовым проектом, для этого склонируем его из репозитория:


git clone https://github.com/almazrafi/TuistDemo.git

Это простой проект iOS-приложения пока без внешних зависимостей, состоит из 1 таргета, имеет 3 конфигурации сборки: Debug, Release и Beta.



План прост: сначала интегрируем Tuist в этот проект, затем попробуем добавить в него внешние зависимости, а в конце посмотрим на возможности для модуляризации.


Фиксация версии Tuist


Первым делом рекомендуется зафиксировать версию Tuist для проекта, выполним команду в корневой папке:


tuist local 1.45.1

После выполнения указанная версия будет записана в файл .tuist-version, и все команды Tuist в этой папке будут выполняться с этой версией инструмента.


Манифест проекта


Следующим шагом необходимо добавить манифест Tuist для нашего проекта, создадим файл Project.swift в корневой папке:


touch Project.swift

Можно сразу открыть этот файл в Xcode, но тогда придётся описывать проект без автодополнения. К счастью, Tuist умеет создавать временную среду для удобного редактирования манифестов, и достаточно выполнить команду:


tuist edit

В результате откроется временный Xcode-проект, в котором можно работать с манифестами, как с обычным проектом на Swift:



Созданный файл Project.swift находится в группе Manifest, добавим в него описание проекта:


import ProjectDescription

let target = Target(
    name: "App",
    platform: .iOS,
    product: .app,
    bundleId: "io.tuist.Demo",
    deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone),
    infoPlist: "App/Info.plist",
    sources: ["App/**"],
    resources: [
        "App/Resources/**",
        "App/**/*.storyboard"
    ],
    dependencies: [],
    settings: Settings(
        configurations: [
            .debug(
                name: "Debug",
                xcconfig: "App/Configurations/Debug.xcconfig"
            ),
            .release(
                name: "Release",
                settings: SettingsDictionary()
                    .bitcodeEnabled(true)
                    .swiftCompilationMode(.wholemodule)
            ),
            .release(
                name: "Beta",
                xcconfig: "App/Configurations/Beta.xcconfig"
            )
        ]
    )
)

let project = Project(
    name: "TuistDemo",
    targets: [target]
)

Наш таргет является iOS-приложением — указываем соответствующие значения в параметрах platform и product. С типом продукта сборки никаких проблем нет, поддерживаются и различные типы фреймворков, и виджеты, и App Clips.


В качестве исходников таргета указываем содержимое папки App в параметре sources. Tuist умный и сам фильтрует файлы ресурсов, которые определяются отдельно в параметре resources. У нашего проекта пока нет зависимостей, поэтому в параметре dependencies передаём пустой массив.


В параметре settings описаны все конфигурации таргета, каждый наследует одну из встроенных конфигураций: либо debug, либо release. Сами настройки сборки можно переопределить, указав путь к файлу xcconfig, либо передав их параметре settings.


Манифест проекта обязательно должен содержать переменную типа Project с описанием проекта, в минимальном виде достаточно указать его название и таргеты в соответствующих параметрах name и targets.


Генерация Xcode-проекта


Теперь для генерации Xcode-проект всё готово, выполним команду в корневой папке:


tuist generate --open

Эта команда создаст новый Xcode-проект вместе с воркспейсом, который откроется в Xcode, благодаря флагу --open.



Все конфигурации, исходники и ресурсы на месте, но появилась новая папка Derived. В ней находятся сгенерированные файлы, в нашем случае это интерфейс доступа к ресурсам.


Интерфейс доступа к ресурсам


Увы, стандартные средства Xcode не предоставляют удобных и безопасных механизмов для доступа к ресурсам. В качестве примера рассмотрим получение цвета из ассетов:


view.backgroundColor = UIColor(named: "Background")

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


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


view.backgroundColor = AppAsset.background.color

Таким образом, благодаря автодополнению, работать с ресурсами становится намного проще. А если их состав изменится, достаточно перегенерировать проект командой tuist generate, и компилятор покажет, что нужно поменять в коде.


Внешние зависимости


С основой разобрались, пора научиться подключать внешние зависимости. Tuist поддерживает все используемые в iOS-разработке менеджеры: Swift Package Manager, Carthage и CocoaPods.


Для примера будем подключать SnapKit, используя Swift Package Manager. Откроем манифест проекта Project.swift и добавим Swift пакет в параметре packages инициализатора Project:


let project = Project(
    name: "TuistDemo",
    packages: [
        .remote(
            url: "https://github.com/SnapKit/SnapKit.git",
            requirement: .upToNextMajor(from: "5.0.1")
        )
    ],
    targets: [target]
)

Далее, необходимо установить зависимость от SnapKit для самого таргета, передадим его в параметре dependencies инициализатора Target:


let target = Target(
    ...
    dependencies: [
        .package(product: "SnapKit")
    ],
    ...
)

Теперь достаточно выполнить команду tuist generate, остальное Tuist сделает сам. После завершения генерации в папке проекта появится файл .package.resolved с фиксированными версиями пакетов.


Модуляризация


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


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


Новый модуль


В демо-проекте уже предусмотрена отдельная папка с исходниками для нового модуля чата, она находится внутри папки Features.



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


Хелперы для описания проектов


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


Эту проблему можно легко решить с помощью переиспользуемых хелперов, которые должны располагаться в папке Tuist/ProjectDescriptionHelpers, добавим её для нашего демо-проекта:


mkdir -p Tuist/ProjectDescriptionHelpers

Следующим шагом создаем файл Project+Feature.swift в этой папке:


touch Tuist/ProjectDescriptionHelpers/Project+Feature.swift

Остаётся выполнить команду tuist edit, найти этот файл в среде редактирования и добавить следующее расширение типа Project:


import ProjectDescription

extension Project {

    public static func feature(
        name: String,
        packages: [Package],
        dependencies: [TargetDependency]
    ) -> Self {
        Self(
            name: name,
            packages: packages,
            targets: [
                Target(
                    name: name,
                    platform: .iOS,
                    product: .staticFramework,
                    bundleId: "io.tuist.Demo.\(name)",
                    deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone),
                    infoPlist: .default,
                    sources: ["Sources/**"],
                    dependencies: dependencies
                )
            ]
        )
    }
}

Метод feature создаёт проекты модулей, которые отличаются только именем, зависимостями и файлами на диске.


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


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


Также стоит обратить внимание на параметр infoPlist, ему передается значение по умолчанию. В этом случае Tuist самостоятельно генерирует файл Info.plist в папке Derived и связывает его с проектом.


Использование хелперов в манифестах


Теперь, когда наш хелпер готов, его можно использовать для описания нового модуля чата, создадим файл манифеста в папке Features/Chat:


touch Features/Chat/Project.swift

Затем откроем среду редактирования командой tuist edit и добавим следующее содержимое в этот манифест:


import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.feature(
    name: "Chat",
    packages: [
        .remote(
            url: "https://github.com/SnapKit/SnapKit.git",
            requirement: .upToNextMajor(from: "5.0.1")
        )
    ],
    dependencies: [.package(product: "SnapKit")]
)

На этом манифест нашего модуля завершён, но этого ещё недостаточно, так как само приложение о нём ничего не знает.


Зависимость от модуля


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


let target = Target(
    ...
    dependencies: [
        .project(target: "Chat", path: "Features/Chat")
    ],
    ...
)

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



По умолчанию Tuist генерирует схемы для всех таргетов, и наш новый модуль не исключение. Это удобно, так как отдельные схемы позволяют не собирать весь проект, чтобы проверить ошибки в одном таргете. Но есть ещё одна фича Tuist, упрощающая разработку в условиях модульного проекта.


Фокус на таргетах


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


Для решения этой проблемы Tuist предлагает генерировать только необходимую часть проекта, убирая всё лишнее. Например, чтобы сфокусироваться на нашем таргете Chat, достаточно выполнить команду:


tuist focus Chat

После её выполнения сразу откроется Xcode, и в нём будет только проект Chat с единственным одноимённым таргетом. Команда фокуса убирает всё, от чего не зависят сами фокусируемые таргеты. Да, команде можно передать несколько таргетов для фокуса:


tuist focus Chat App

Но и это ещё не всё.


Кэширование артефактов


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


tuist cache warm
tuist focus App

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


Если зависимости изменились, можно повторно "подогреть" кэш командой tuist cache warm, и Tuist сам найдёт изменения и пересоберёт только необходимые таргеты.



Подводя итог


Продемонстрировать все возможности Tuist в одной статье невозможно, и без внимания осталось ещё много вкусного:


  • Генерация новых компонентов и модулей из шаблонов
  • Запуск тестов с возможностью пропускать таргеты без изменений
  • Скрипты на Swift для замены Ruby и Shell кода
  • Автоматизация настройки окружения
  • Построение графа зависимостей проекта
  • Генерация документации

Это не значит, что Tuist делает всё сам, интеграция потребует больших ресурсов. Но если цель — сделать структуру проекта удобной и масштабируемой, то потраченные ресурсы быстро себя оправдают.


В нашем проекте Tuist уже заменил кучу велосипедов на Ruby, которые поддерживались только узким кругом разработчиков. Инфраструктура стала дешёвой за счет Swift, а работа с проектом — настоящим удовольствием.


Что с поддержкой?


С поддержкой нет никаких проблем, обновления выходят с завидной частотой. Tuist — это тот случай, когда issues смотришь не только для поиска проблем, но и для планирования улучшений.


Например, мы очень ждём новый универсальный интерфейс для подключения сторонних зависимостей, который позволит абстрагировать проект от менеджеров Carthage, CocoaPods и Swift Package Manager.


На что обратить внимание?


Следует привыкнуть, что Xcode-проект постоянно генерируется, значит, изменения настроек проекта следует производить в манифестах Tuist.


А что касается файловой структуры проекта, тут всё по-старому: добавление, удаление и перемещение файлов происходит как обычно, так как их индексация не имеет значения.


На этом всё.


Буду рад обратной связи в комментариях. Пока!

Tags:
Hubs:
Total votes 14: ↑13 and ↓1+16
Comments9

Articles

Information

Website
hh.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия