Monorepositório com Swift: organização de código na prática
\n\nO desenvolvimento iOS moderno enfrenta cada vez mais o desafio de escalar a base de código. Quando um projeto cresce para dezenas de módulos, bibliotecas compartilhadas e vários aplicativos (iOS, macOS, watchOS), o modelo tradicional com repositórios separados (polyrepo) começa a desacelerar o desenvolvimento. O monorepositório vem em socorro — um repositório único que contém o código de todos os componentes do ecossistema. Neste artigo, vamos analisar na prática como organizar um monorepositório para projetos Swift, usando ferramentas modernas: Swift Package Manager (SPM), Tuist e pipelines CI/CD eficientes.
\n\n1. Por que um monorepositório no ecossistema Swift?
\n\nAntes de mergulhar na implementação, é importante entender as principais vantagens da abordagem:
\n\n- \n
- Versionamento unificado — todos os módulos e aplicativos estão em um único commit, eliminando o problema do "inferno das dependências" (dependency hell). \n
- Refatoração simplificada — uma alteração de API em uma biblioteca é imediatamente visível para todos os consumidores; o compilador destacará erros dentro de um único PR. \n
- Navegação transparente — os desenvolvedores veem todo o código do projeto, sem precisar alternar entre repositórios. \n
- Otimização de CI/CD — é possível configurar a compilação apenas dos módulos alterados (targeted builds), economizando tempo e recursos. \n
No entanto, um monorepositório exige disciplina: uma arquitetura modular clara e ferramentas que evitem o caos. Para Swift, a melhor escolha hoje é o Tuist — um utilitário que gerencia projetos Xcode de forma declarativa, ou o puro Swift Package Manager para uma estrutura modular.
\n\n2. Estrutura do monorepositório com Swift Package Manager
\n\nVamos começar com a organização básica. Suponha que temos dois aplicativos: MyAppiOS e MyAppMacOS, além de um conjunto de bibliotecas internas. A estrutura de diretórios pode ser assim:
Monorepo/\n├── Apps/\n│ ├── MyAppiOS/\n│ │ ├── Sources/\n│ │ └── Package.swift (ou 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 (manifesto raiz)\n└── Tuist/ (se estivermos usando Tuist)\n └── Project.swift\n\n\nO Package.swift raiz conecta tudo. Exemplo de manifesto:
// 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 // Dependências 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