Монорепозиторий с Swift: организация кода на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Монорепозиторий с Swift: организация кода на практике

Современная iOS-разработка всё чаще сталкивается с вызовом масштабирования кодовой базы. Когда проект вырастает до десятков модулей, общих библиотек и нескольких приложений (iOS, macOS, watchOS), традиционная модель с отдельными репозиториями (polyrepo) начинает тормозить разработку. На помощь приходит монорепозиторий — единый репозиторий, содержащий код всех компонентов экосистемы. В этой статье мы на практике разберём, как организовать монорепозиторий для Swift-проектов, используя современные инструменты: Swift Package Manager (SPM), Tuist и эффективные CI/CD-пайплайны.

1. Зачем нужен монорепозиторий в Swift-экосистеме?

Прежде чем погружаться в реализацию, важно понять ключевые преимущества подхода:

  • Единая версионность — все модули и приложения находятся в одном коммите, что исключает проблему "ада зависимостей" (dependency hell).
  • Упрощённый рефакторинг — изменение API в библиотеке сразу видно во всех потребителях; компилятор подсветит ошибки в рамках одного PR.
  • Прозрачная навигация — разработчики видят весь код проекта, не переключаясь между репозиториями.
  • Оптимизация CI/CD — можно настроить сборку только изменённых модулей (targeted builds), экономя время и ресурсы.

Однако монорепозиторий требует дисциплины: чёткой модульной архитектуры и инструментов, предотвращающих хаос. Для Swift лучшим выбором сегодня является Tuist — утилита, которая управляет Xcode-проектами декларативно, или чистый Swift Package Manager для модульной структуры.

2. Структура монорепозитория на Swift Package Manager

Начнём с базовой организации. Допустим, у нас есть два приложения: MyAppiOS и MyAppMacOS, а также набор внутренних библиотек. Файловая структура может выглядеть так:

Monorepo/
├── Apps/
│   ├── MyAppiOS/
│   │   ├── Sources/
│   │   └── Package.swift (или project.yml для Tuist)
│   └── MyAppMacOS/
│       ├── Sources/
│       └── Package.swift
├── Libraries/
│   ├── Networking/
│   │   ├── Sources/
│   │   └── Package.swift
│   ├── UIComponents/
│   │   ├── Sources/
│   │   └── Package.swift
│   └── Core/
│       ├── Sources/
│       └── Package.swift
├── Tests/
│   ├── NetworkingTests/
│   └── UIComponentsTests/
├── Package.swift (корневой манифест)
└── Tuist/ (если используем Tuist)
    └── Project.swift

Корневой Package.swift связывает всё воедино. Пример манифеста:

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyMonorepo",
    platforms: [
        .iOS(.v16), .macOS(.v13)
    ],
    products: [
        .library(name: "Networking", targets: ["Networking"]),
        .library(name: "UIComponents", targets: ["UIComponents"]),
        .library(name: "Core", targets: ["Core"]),
        .executable(name: "MyAppiOS", targets: ["MyAppiOS"]),
        .executable(name: "MyAppMacOS", targets: ["MyAppMacOS"])
    ],
    dependencies: [
        // Внешние зависимости
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
    ],
    targets: [
        .target(
            name: "Networking",
            dependencies: ["Core", .product(name: "Alamofire", package: "Alamofire")],
            path: "Libraries/Networking/Sources"
        ),
        .target(
            name: "UIComponents",
            dependencies: ["Core"],
            path: "Libraries/UIComponents/Sources"
        ),
        .target(
            name: "Core",
            path: "Libraries/Core/Sources"
        ),
        .executableTarget(
            name: "MyAppiOS",
            dependencies: ["Networking", "UIComponents"],
            path: "Apps/MyAppiOS/Sources"
        ),
        .executableTarget(
            name: "MyAppMacOS",
            dependencies: ["Networking", "UIComponents"],
            path: "Apps/MyAppMacOS/Sources"
        ),
        // Тесты
        .testTarget(
            name: "NetworkingTests",
            dependencies: ["Networking"],
            path: "Tests/NetworkingTests"
        )
    ]
)

Важно: каждый внутренний модуль имеет собственный Package.swift только если он может быть опубликован отдельно. В рамках монорепозитория достаточно одного корневого манифеста — это упрощает управление версиями.

3. Настройка Tuist для монорепозитория

Если вы предпочитаете Xcode-проекты, а не SPM-сборку, Tuist станет вашим лучшим другом. Он генерирует .xcodeproj из декларативного файла Project.swift. Пример для нашего монорепозитория:

// Tuist/Project.swift
import ProjectDescription

let project = Project(
    name: "Monorepo",
    targets: [
        // iOS приложение
        Target(
            name: "MyAppiOS",
            platform: .iOS,
            product: .app,
            bundleId: "com.example.myappios",
            infoPlist: .default,
            sources: ["Apps/MyAppiOS/Sources/**"],
            dependencies: [
                .target(name: "Networking"),
                .target(name: "UIComponents")
            ]
        ),
        // macOS приложение
        Target(
            name: "MyAppMacOS",
            platform: .macOS,
            product: .app,
            bundleId: "com.example.myappmacos",
            infoPlist: .default,
            sources: ["Apps/MyAppMacOS/Sources/**"],
            dependencies: [
                .target(name: "Networking"),
                .target(name: "UIComponents")
            ]
        ),
        // Библиотеки
        Target(
            name: "Networking",
            platform: .iOS,
            product: .framework,
            bundleId: "com.example.networking",
            sources: ["Libraries/Networking/Sources/**"],
            dependencies: [
                .target(name: "Core")
            ]
        ),
        Target(
            name: "UIComponents",
            platform: .iOS,
            product: .framework,
            bundleId: "com.example.uicomponents",
            sources: ["Libraries/UIComponents/Sources/**"],
            dependencies: [
                .target(name: "Core")
            ]
        ),
        Target(
            name: "Core",
            platform: .iOS,
            product: .framework,
            bundleId: "com.example.core",
            sources: ["Libraries/Core/Sources/**"]
        )
    ]
)

Блоги

Рекомендаций по книгам