Monorepo avec Swift : organisation du code en pratique
\n\nLe développement iOS moderne est de plus en plus confronté au défi de la mise à l'échelle de la base de code. Lorsqu'un projet atteint des dizaines de modules, des bibliothèques partagées et plusieurs applications (iOS, macOS, watchOS), le modèle traditionnel de dépôts séparés (polyrepo) commence à ralentir le développement. Le monorepo vient à la rescousse — un dépôt unique contenant le code de tous les composants de l'écosystème. Dans cet article, nous allons voir en pratique comment organiser un monorepo pour des projets Swift, en utilisant des outils modernes : Swift Package Manager (SPM), Tuist et des pipelines CI/CD efficaces.
\n\n1. Pourquoi un monorepo dans l'écosystème Swift ?
\n\nAvant de plonger dans l'implémentation, il est important de comprendre les principaux avantages de l'approche :
\n\n- \n
- Versionnage unifié — tous les modules et applications se trouvent dans un seul commit, ce qui élimine le problème de "l'enfer des dépendances" (dependency hell). \n
- Refactorisation simplifiée — une modification de l'API dans une bibliothèque est immédiatement visible par tous les consommateurs ; le compilateur mettra en évidence les erreurs dans une seule PR. \n
- Navigation transparente — les développeurs voient tout le code du projet sans avoir à basculer entre les dépôts. \n
- Optimisation du CI/CD — il est possible de configurer la construction uniquement des modules modifiés (targeted builds), économisant du temps et des ressources. \n
Cependant, un monorepo exige de la discipline : une architecture modulaire claire et des outils pour éviter le chaos. Pour Swift, le meilleur choix aujourd'hui est Tuist — un utilitaire qui gère les projets Xcode de manière déclarative, ou le Swift Package Manager pur pour une structure modulaire.
\n\n2. Structure d'un monorepo avec Swift Package Manager
\n\nCommençons par l'organisation de base. Supposons que nous ayons deux applications : MyAppiOS et MyAppMacOS, ainsi qu'un ensemble de bibliothèques internes. La structure des fichiers pourrait ressembler à ceci :
Monorepo/\n├── Apps/\n│ ├── MyAppiOS/\n│ │ ├── Sources/\n│ │ └── Package.swift (ou project.yml pour Tuist)\n│ └── MyAppMacOS/\n│ ├── Sources/\n│ └── Package.swift\n├── Libraries/\n│ ├── Networking/\n│ │ ├── Sources/\n│ │ └── Package.swift\n│ ├── UIComponents/\n│ │ ├── Sources/\n│ │ └── Package.swift\n│ └── Core/\n│ ├── Sources/\n│ └── Package.swift\n├── Tests/\n│ ├── NetworkingTests/\n│ └── UIComponentsTests/\n├── Package.swift (manifeste racine)\n└── Tuist/ (si nous utilisons Tuist)\n └── Project.swift\n\n\nLe Package.swift racine relie le tout. Exemple de manifeste :
// swift-tools-version:5.9\nimport PackageDescription\n\nlet package = Package(\n name: "MyMonorepo",\n platforms: [\n .iOS(.v16), .macOS(.v13)\n ],\n products: [\n .library(name: "Networking", targets: ["Networking"]),\n .library(name: "UIComponents", targets: ["UIComponents"]),\n .library(name: "Core", targets: ["Core"]),\n .executable(name: "MyAppiOS", targets: ["MyAppiOS"]),\n .executable(name: "MyAppMacOS", targets: ["MyAppMacOS"])\n ],\n dependencies: [\n // Dépendances externes\n .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")\n ],\n targets: [\n .target(\n name: "Networking",\n dependencies: ["Core", .product(name: "Alamofire", package: "Alamofire")],\n path: "Libraries/Networking/Sources"\n ),\n .target(\n name: "UIComponents",\n dependencies: ["Core"],\n path: "Libraries/UIComponents/Sources"\n ),\n .target(\n name: "Core",\n path: "Libraries/Core/Sources"\n ),\n .executableTarget(\n name: "MyAppiOS",\n dependencies: ["Networking", "UIComponents"],\n path: "Apps/MyAppiOS/Sources"\n