介绍:为什么在 Swift 中使用 Monorepo?
iOS 和 macOS 开发经常面临一个难题:如何组织代码,使其保持可扩展、可复用且易于维护?传统的多仓库方法(multirepo)会导致碎片化、依赖重复以及协调变更的复杂性。解决方案是 monorepo。
Monorepo 是一个包含项目所有组件的单一仓库:应用程序、库、构建工具和配置。对于 Swift 生态系统,这种方法尤其相关,因为 Swift Package Manager (SPM) 从 5.3+ 版本开始就很好地支持本地包和模块化架构。
在本文中,我们将探讨如何为 Swift 项目组织 monorepo,介绍结构、工具(Tuist, XcodeGen)以及带有代码示例的最佳实践。
1. Monorepo 对 Swift 的优势
在进入实现之前,让我们先看看关键优势:
- 统一的依赖版本 — 所有模块使用相同版本的库,消除冲突。
- 原子提交 — 一个包中的 API 更改会立即反映在所有依赖模块上,无需在仓库之间同步。
- 简化 CI/CD — 一个流水线,一个配置,构建缓存。
- 代码复用 — 通用工具、模型、网络层可轻松用于所有目标(iOS, macOS, watchOS)。
- 代码审查 — 项目中不同部分的更改可以在一个 PR 中看到。
但也有缺点:仓库大小、克隆时间、需要精心设计的结构。但对于中等规模及以上的团队来说,monorepo 通常是最佳选择。
2. Swift Monorepo 的结构
考虑一个包含 iOS 应用和 macOS 应用以及共享库的项目的典型结构:
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 (根目录)├── Tuist.swift (如果使用 Tuist)└── README.md结构说明:
Apps/— 存放最终应用程序的目录。每个应用都是一个独立的 Swift 包,依赖于Packages/中的包。Packages/— 所有内部库和模块。每个包都有自己的Package.swift,可以独立测试。Tools/— 构建脚本、linter、代码生成器。- 根目录下的
Package.swift(或 Tuist/XcodeGen 配置) — 描述 workspace 和包之间的依赖关系。
2.1 根目录 Package.swift 示例 (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