Introdução: por que um monorepositório em Swift?
O desenvolvimento para iOS e macOS frequentemente enfrenta um dilema: como organizar o código para que ele permaneça escalável, reutilizável e facilmente mantível? A abordagem tradicional com múltiplos repositórios separados (multirrepositório) leva à fragmentação, duplicação de dependências e dificuldades na coordenação de mudanças. A solução é o monorepositório (monorepo).
Um monorepositório é um repositório único que contém todos os componentes do projeto: aplicativos, bibliotecas, ferramentas de build e configurações. Para o ecossistema Swift, essa abordagem é especialmente relevante graças ao Swift Package Manager (SPM), que desde a versão 5.3+ oferece excelente suporte para pacotes locais e arquitetura modular.
Neste artigo, vamos analisar como organizar um monorepositório para projetos Swift, explorar a estrutura, ferramentas (Tuist, XcodeGen) e melhores práticas com exemplos de código.
1. Vantagens do monorepositório para Swift
Antes de partir para a implementação, vejamos os principais benefícios:
- Versão única de dependências — todos os módulos usam a mesma versão das bibliotecas, eliminando conflitos.
- Commits atômicos — uma alteração de API em um pacote reflete imediatamente em todos os módulos dependentes, sem necessidade de sincronização entre repositórios.
- Simplificação de CI/CD — um único pipeline, uma configuração, cache de builds.
- Reutilização de código — utilitários compartilhados, modelos, camadas de rede facilmente acessíveis para todos os targets (iOS, macOS, watchOS).
- Code review — alterações em diferentes partes do projeto são visíveis em um único PR.
No entanto, também há desvantagens: tamanho do repositório, tempo de clonagem, necessidade de uma estrutura bem planejada. Mas para equipes de médio porte ou maiores, o monorepositório geralmente se mostra a escolha ideal.
2. Estrutura do monorepositório em Swift
Vamos considerar uma estrutura típica para um projeto que inclui um aplicativo iOS e um aplicativo macOS com uma biblioteca compartilhada:
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 (raiz)├── Tuist.swift (se usarmos Tuist)└── README.mdExplicação da estrutura:
Apps/— diretório com os aplicativos finais. Cada aplicativo é um pacote Swift separado que depende dos pacotes emPackages/.Packages/— todas as bibliotecas e módulos internos. Cada pacote tem seu próprioPackage.swifte pode ser testado de forma independente.Tools/— scripts de build, linters, geração de código.- O
Package.swiftraiz (ou configuração Tuist/XcodeGen) — descreve o workspace e as conexões entre os pacotes.
2.1 Exemplo de Package.swift raiz (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
```