使用 Swift 的单一代码库:代码组织实践
\n\n现代 iOS 开发越来越频繁地面临代码库扩展的挑战。当项目增长到包含数十个模块、公共库以及多个应用程序(iOS、macOS、watchOS)时,传统的独立仓库(polyrepo)模式会拖慢开发速度。这时,单一代码库——一个包含生态系统所有组件代码的统一仓库——便应运而生。在本文中,我们将通过实践来探讨如何使用现代工具:Swift Package Manager (SPM)、Tuist 和高效的 CI/CD 管道,为 Swift 项目组织单一代码库。
\n\n1. 为什么 Swift 生态系统需要单一代码库?
\n\n在深入实现细节之前,理解这种方法的关键优势很重要:
\n\n- \n
- 统一版本管理——所有模块和应用程序都在同一个提交中,从而消除了“依赖地狱”问题。 \n
- 简化重构——库中 API 的更改会立即对所有使用者可见;编译器会在同一个 PR 中高亮显示错误。 \n
- 透明的导航——开发人员可以看到项目的所有代码,无需在仓库之间切换。 \n
- 优化 CI/CD——可以配置仅构建已更改的模块(定向构建),从而节省时间和资源。 \n
然而,单一代码库需要纪律:清晰的模块化架构和防止混乱的工具。对于 Swift 来说,如今的最佳选择是 Tuist——一个以声明方式管理 Xcode 项目的工具,或者使用纯 Swift Package Manager 来构建模块化结构。
\n\n2. 基于 Swift Package Manager 的单一代码库结构
\n\n我们从基本组织开始。假设我们有两个应用程序:MyAppiOS 和 MyAppMacOS,以及一组内部库。文件结构可能如下所示:
Monorepo/\n├── Apps/\n│ ├── MyAppiOS/\n│ │ ├── Sources/\n│ │ └── Package.swift (或用于 Tuist 的 project.yml)\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 (根清单文件)\n└── Tuist/ (如果使用 Tuist)\n └── Project.swift\n\n\n根目录下的 Package.swift 将所有内容连接在一起。清单文件示例:
// 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 // 外部依赖\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