Introducción: ¿por qué un monorepositorio en Swift?
El desarrollo para iOS y macOS a menudo se enfrenta a un dilema: ¿cómo organizar el código para que siga siendo escalable, reutilizable y fácil de mantener? El enfoque tradicional de múltiples repositorios separados (multirrepositorio) conduce a la fragmentación, duplicación de dependencias y dificultades para coordinar los cambios. La solución es el monorepositorio (monorepo).
Un monorepositorio es un único repositorio que contiene todos los componentes del proyecto: aplicaciones, bibliotecas, herramientas de compilación y configuraciones. Para el ecosistema Swift, este enfoque es especialmente relevante gracias a Swift Package Manager (SPM), que desde la versión 5.3+ admite excelentemente paquetes locales y arquitectura modular.
En este artículo, analizaremos cómo organizar un monorepositorio para proyectos Swift, consideraremos la estructura, las herramientas (Tuist, XcodeGen) y las mejores prácticas con ejemplos de código.
1. Ventajas del monorepositorio para Swift
Antes de pasar a la implementación, consideremos los beneficios clave:
- Versión única de dependencias — todos los módulos usan la misma versión de las bibliotecas, eliminando conflictos.
- Commits atómicos — un cambio de API en un paquete se refleja inmediatamente en todos los módulos dependientes, sin necesidad de sincronización entre repositorios.
- Simplificación de CI/CD — un pipeline, una configuración, almacenamiento en caché de compilaciones.
- Reutilización de código — utilidades comunes, modelos, capas de red fácilmente accesibles para todos los targets (iOS, macOS, watchOS).
- Code review — los cambios en diferentes partes del proyecto son visibles en un solo PR.
Sin embargo, también hay desventajas: el tamaño del repositorio, el tiempo de clonación, la necesidad de una estructura bien pensada. Pero para equipos de tamaño mediano y superior, el monorepositorio suele ser la opción óptima.
2. Estructura del monorepositorio en Swift
Consideremos una estructura típica para un proyecto que incluye una aplicación iOS y una aplicación macOS con una biblioteca compartida:
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 (raíz)├── Tuist.swift (si usamos Tuist)└── README.mdExplicación de la estructura:
Apps/— directorio con las aplicaciones finales. Cada aplicación es un paquete Swift independiente que depende de los paquetes enPackages/.Packages/— todas las bibliotecas y módulos internos. Cada paquete tiene su propioPackage.swifty puede ser probado de forma independiente.Tools/— scripts de compilación, linters, generación de código.- El
Package.swiftraíz (o la configuración de Tuist/XcodeGen) — describe el workspace y las conexiones entre paquetes.
2.1 Ejemplo de Package.swift raíz (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