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

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

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

Начать курс

Введение: зачем монорепозиторий в Swift?

Разработка под iOS и macOS часто сталкивается с дилеммой: как организовать код, чтобы он оставался масштабируемым, переиспользуемым и легко поддерживаемым? Традиционный подход с множеством отдельных репозиториев (мультирепозиторий) приводит к фрагментации, дублированию зависимостей и сложностям с координацией изменений. Решение — монорепозиторий (monorepo).

Монорепозиторий — это единый репозиторий, содержащий все компоненты проекта: приложения, библиотеки, инструменты сборки и конфигурации. Для экосистемы Swift этот подход особенно актуален благодаря Swift Package Manager (SPM), который с версии 5.3+ отлично поддерживает локальные пакеты и модульную архитектуру.

В этой статье мы разберем, как организовать монорепозиторий для Swift-проектов, рассмотрим структуру, инструменты (Tuist, XcodeGen) и лучшие практики с примерами кода.

1. Преимущества монорепозитория для Swift

Перед тем как перейти к реализации, рассмотрим ключевые плюсы:

  • Единая версия зависимостей — все модули используют одну и ту же версию библиотек, исключая конфликты.
  • Атомарные коммиты — изменение API в одном пакете сразу отражается на всех зависимых модулях, без необходимости синхронизации между репозиториями.
  • Упрощение CI/CD — один пайплайн, одна конфигурация, кэширование сборок.
  • Переиспользование кода — общие утилиты, модели, сетевые слои легко доступны для всех таргетов (iOS, macOS, watchOS).
  • Код-ревью — изменения в разных частях проекта видны в одном PR.

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

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

Рассмотрим типовую структуру для проекта, включающего iOS-приложение и macOS-приложение с общей библиотекой:

MyProject/
├── Apps/
│   ├── iOSApp/
│   │   ├── Sources/
│   │   │   └── AppDelegate.swift
│   │   ├── Resources/
│   │   └── Package.swift
│   └── macOSApp/
│       ├── Sources/
│       │   └── AppDelegate.swift
│       ├── Resources/
│       └── Package.swift
├── Packages/
│   ├── Core/
│   │   ├── Sources/
│   │   │   ├── Core.swift
│   │   │   └── Extensions/
│   │   ├── Tests/
│   │   └── Package.swift
│   ├── Networking/
│   │   ├── Sources/
│   │   │   └── NetworkManager.swift
│   │   ├── Tests/
│   │   └── Package.swift
│   └── UIComponents/
│       ├── Sources/
│       │   └── CustomButton.swift
│       ├── Tests/
│       └── Package.swift
├── Tools/
│   └── Scripts/
│       └── build.sh
├── .github/
│   └── workflows/
│       └── ci.yml
├── Package.swift (корневой)
├── Tuist.swift (если используем Tuist)
└── README.md

Пояснение структуры:

  • Apps/ — каталог с конечными приложениями. Каждое приложение — это отдельный Swift-пакет, который зависит от пакетов из Packages/.
  • Packages/ — все внутренние библиотеки и модули. Каждый пакет имеет свой Package.swift и может быть протестирован независимо.
  • Tools/ — скрипты сборки, линтеры, кодогенерация.
  • Корневой Package.swift (или конфигурация Tuist/XcodeGen) — описывает workspace и связи между пакетами.

2.1 Пример корневого Package.swift (SPM)

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyProject",
    platforms: [
        .iOS(.v16),
        .macOS(.v13)
    ],
    products: [
        .library(name: "Core", targets: ["Core"]),
        .library(name: "Networking", targets: ["Networking"]),
        .library(name: "UIComponents", targets: ["UIComponents"]),
    ],
    dependencies: [
        .package(path: "Packages/Core"),
        .package(path: "Packages/Networking"),
        .package(path: "Packages/UIComponents"),
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
    ],
    targets: [
        .target(
            name: "iOSApp",
            dependencies: [
                "Core",
                "Networking",
                "UIComponents",
                .product(name: "Alamofire", package: "Alamofire")
            ],
            path: "Apps/iOSApp/Sources"
        ),
        .target(
            name: "macOSApp",
            dependencies: [
                "Core",
                "Networking",
                .product(name: "Alamofire", package: "Alamofire")
            ],
            path: "Apps/macOSApp/Sources"
        ),
        .testTarget(
            name: "CoreTests",
            dependencies: ["Core"],
            path: "Packages/Core/Tests"
        )
    ]
)

3. Инструменты для управления монорепозиторием Swift

Хотя SPM сам по себе достаточен для базовой работы, существуют инструменты, которые упрощают генерацию Xcode-проектов и управление сложными конфигурациями.

3.1 Tuist

Tuist — это инструмент для генерации и управления Xcode-проектами на основе Swift-конфигурации. Он идеально подходит для монорепозиториев, так как позволяет описывать все модули и их зависимости в одном файле Project.swift.

// Tuist/Project.swift
import ProjectDescription

let project = Project(
    name: "MyProject",
    targets: [
        .target(
            name: "iOSApp",
            destinations: .iOS,
            product: .app,
            bundleId: "com.example.iosapp",
            infoPlist: .default,
            sources: ["Apps/iOSApp/Sources/**"],
            dependencies: [
                .project(target: "Core", path: "../Packages/Core"),
                .project(target: "Networking", path: "../Packages/Networking")
            ]
        ),
        .target(
            name: "Core",
            destinations: [.iOS, .macOS],
            product: .framework,
            bundleId: "                

Блоги

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