Introduction : pourquoi un monorepo en Swift ?
Le développement iOS et macOS est souvent confronté à un dilemme : comment organiser le code pour qu'il reste évolutif, réutilisable et facile à maintenir ? L'approche traditionnelle avec plusieurs dépôts distincts (multirepo) entraîne une fragmentation, une duplication des dépendances et des difficultés de coordination des changements. La solution est le monorepo (monorepository).
Un monorepo est un dépôt unique contenant tous les composants du projet : applications, bibliothèques, outils de build et configurations. Pour l'écosystème Swift, cette approche est particulièrement pertinente grâce à Swift Package Manager (SPM), qui, depuis la version 5.3+, prend parfaitement en charge les packages locaux et l'architecture modulaire.
Dans cet article, nous allons voir comment organiser un monorepo pour des projets Swift, examiner la structure, les outils (Tuist, XcodeGen) et les meilleures pratiques avec des exemples de code.
1. Avantages du monorepo pour Swift
Avant de passer à l'implémentation, examinons les principaux avantages :
- Version unique des dépendances — tous les modules utilisent la même version des bibliothèques, éliminant les conflits.
- Commits atomiques — un changement d'API dans un package se répercute immédiatement sur tous les modules dépendants, sans nécessité de synchronisation entre les dépôts.
- Simplification du CI/CD — un seul pipeline, une seule configuration, mise en cache des builds.
- Réutilisation du code — les utilitaires communs, modèles, couches réseau sont facilement accessibles pour toutes les cibles (iOS, macOS, watchOS).
- Code review — les modifications dans différentes parties du projet sont visibles dans une seule PR.
Cependant, il y a aussi des inconvénients : la taille du dépôt, le temps de clonage, la nécessité d'une structure bien pensée. Mais pour les équipes de taille moyenne et plus, le monorepo s'avère souvent être le choix optimal.
2. Structure d'un monorepo en Swift
Examinons une structure typique pour un projet comprenant une application iOS et une application macOS avec une bibliothèque partagée :
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 (racine)├── Tuist.swift (si nous utilisons Tuist)└── README.mdExplication de la structure :
Apps/— répertoire contenant les applications finales. Chaque application est un package Swift distinct qui dépend des packages dansPackages/.Packages/— toutes les bibliothèques et modules internes. Chaque package a son proprePackage.swiftet peut être testé indépendamment.Tools/— scripts de build, linters, génération de code.- Le
Package.swiftracine (ou la configuration Tuist/XcodeGen) — décrit le workspace et les liens entre les packages.
2.1 Exemple de Package.swift racine (SPM)
// swift-tools-version:5.9import PackageDescription
let package = Package( name: "MyProject", platforms: [ .iOS(.v16), .macOS(.v13) ], products: [ .library(name: "Core", targets: ["Core"]), .library(name: "Networking", targets: ["Networking"]), .library(name: "UICompon