Введение: зачем монорепозиторий в 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: "