Monorepositorio con Swift: organización de código en la práctica
\n\nEl desarrollo moderno de iOS se enfrenta cada vez más al desafío de escalar la base de código. Cuando un proyecto crece hasta tener decenas de módulos, bibliotecas compartidas y varias aplicaciones (iOS, macOS, watchOS), el modelo tradicional con repositorios separados (polyrepo) comienza a ralentizar el desarrollo. En su ayuda viene el monorepositorio: un repositorio único que contiene el código de todos los componentes del ecosistema. En este artículo analizaremos en la práctica cómo organizar un monorepositorio para proyectos Swift, utilizando herramientas modernas: Swift Package Manager (SPM), Tuist y pipelines eficientes de CI/CD.
\n\n1. ¿Por qué es necesario un monorepositorio en el ecosistema Swift?
\n\nAntes de sumergirnos en la implementación, es importante entender las ventajas clave del enfoque:
\n\n- \n
- Versión única — todos los módulos y aplicaciones están en un mismo commit, lo que elimina el problema del "infierno de dependencias" (dependency hell). \n
- Refactorización simplificada — un cambio de API en una biblioteca se ve inmediatamente en todos los consumidores; el compilador resaltará los errores dentro de un mismo PR. \n
- Navegación transparente — los desarrolladores ven todo el código del proyecto sin tener que cambiar entre repositorios. \n
- Optimización de CI/CD — se puede configurar la compilación solo de los módulos modificados (targeted builds), ahorrando tiempo y recursos. \n
Sin embargo, un monorepositorio requiere disciplina: una arquitectura modular clara y herramientas que eviten el caos. Para Swift, la mejor opción hoy en día es Tuist, una utilidad que gestiona proyectos Xcode de forma declarativa, o el Swift Package Manager puro para una estructura modular.
\n\n2. Estructura de un monorepositorio con Swift Package Manager
\n\nEmpecemos con la organización básica. Supongamos que tenemos dos aplicaciones: MyAppiOS y MyAppMacOS, además de un conjunto de bibliotecas internas. La estructura de archivos podría verse así:
Monorepo/\n├── Apps/\n│ ├── MyAppiOS/\n│ │ ├── Sources/\n│ │ └── Package.swift (o project.yml para 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 (manifiesto raíz)\n└── Tuist/ (si usamos Tuist)\n └── Project.swift\n\n\nEl Package.swift raíz lo conecta todo. Ejemplo de manifiesto:
// 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 // Dependencias externas\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