Swift Package Manager (SPM) 终极教程:从零开始管理项目依赖
在现代软件开发中,代码复用是提高效率和保证质量的关键。我们很少会从零开始构建一个应用程序的所有部分,而是会站在巨人的肩膀上,使用社区中已经存在并经过验证的第三方库。在 Apple 生态中,管理这些第三方库(或称“依赖”)的工具经历了从手动拖拽、CocoaPods 到 Carthage 的演进,最终,Apple 亲自下场,推出了官方的、深度集成在开发工具链中的解决方案——Swift Package Manager (SPM)。
SPM 不仅仅是一个依赖管理工具,它还是一个构建系统、测试和打包工具。它的出现极大地简化了 Swift 项目的依赖管理流程,使其变得前所未有的简单、安全和高效。本文将作为一份详尽的指南,带你从零开始,深入探索 SPM 的世界,无论你是 iOS/macOS 应用开发者,还是服务端 Swift 工程师,都能从中获益。
目录
-
第一章:核心概念——理解 SPM 的基石
- 什么是包 (Package)?
- 清单文件
Package.swift
- 模块 (Module)、目标 (Target) 与产品 (Product)
- 依赖 (Dependency) 与版本控制
-
第二章:Xcode 实战——在应用中集成第三方库
- 添加第一个 SPM 依赖 (以 Alamofire 为例)
- 理解依赖规则 (版本、分支、Commit)
- 更新、解析和移除依赖
- 在代码中使用依赖
-
第三章:创建你的第一个 SPM 包
- 为什么需要创建自己的包?
- 使用 Xcode 创建一个新包
- 使用命令行创建新包
- 深入解析
Package.swift
的配置
-
第四章:SPM 进阶技巧
- 本地包依赖:加速开发与调试
- 二进制依赖:集成 XCFramework
- 资源管理:在包中包含图片、数据等文件
- 插件 (Plugins):扩展你的构建流程
-
第五章:命令行中的 SPM
- 常用 SPM 命令详解 (
build
,test
,run
等) - 为非 Xcode 项目(如服务端 Swift)管理依赖
- 常用 SPM 命令详解 (
-
总结:拥抱 SPM,迈向现代化 Swift 开发
第一章:核心概念——理解 SPM 的基石
在开始使用 SPM 之前,我们必须先理解它的几个核心概念。这些概念是 SPM 工作方式的基础。
1.1 什么是包 (Package)?
一个 包 (Package) 是 SPM 的基本单位。它是一组 Swift 源代码文件和一个名为 Package.swift
的清单文件的集合。这个包可以被组织成一个或多个模块 (Module),并可以定义出可供其他包使用的产品 (Product)。简单来说,一个包就是一个可分发的、包含了代码和构建说明的代码库。最常见的形式就是一个 Git 仓库。
1.2 清单文件 Package.swift
Package.swift
是一个 SPM 包的灵魂。与 CocoaPods 的 Podfile
或 Carthage 的 Cartfile
不同,它不是一个静态的配置文件(如 JSON 或 YAML),而是一个用 Swift 语言编写的可执行文件。这带来了巨大的灵活性和强大的功能。
在这个文件中,你将用 Swift 代码来描述你的包:
* 包的名称
* 支持的平台和版本
* 它对外提供哪些产品(库或可执行文件)
* 它依赖哪些其他的包
* 如何组织内部的源代码(目标)
1.3 模块 (Module)、目标 (Target) 与产品 (Product)
这三个概念关系紧密,常常一起出现:
-
目标 (Target):一个目标定义了如何构建一组源文件。它可以被编译成一个模块。一个包可以包含多个目标。常见的目标类型有:
.target
: 常规目标,可以是一个库或可执行文件的源码集合。.executableTarget
: 一个特殊的目标,会生成一个可执行文件。.testTarget
: 用于存放测试代码的目标,它通常依赖于一个或多个常规目标。.binaryTarget
: 用于分发预编译好的二进制文件(如 XCFramework)。
-
模块 (Module):当一个目标被成功编译后,它就成了一个模块。在 Swift 代码中,你通过
import
关键字来使用一个模块。例如import Alamofire
,这里的Alamofire
就是一个模块。 -
产品 (Product):产品是目标构建完成后,最终产出的可供外部使用的工件。一个包可以定义多个产品,每个产品由一个或多个目标构成。主要的产品类型有:
.library
: 声明一个库,供其他包或 App 导入和使用。这是最常见的产品类型。.executable
: 声明一个可执行文件。
关系梳理:你的 源文件
被组织在 目标 (Target)
中,目标
被编译成 模块 (Module)
,最终通过 产品 (Product)
的形式暴露给外部世界。
1.4 依赖 (Dependency) 与版本控制
一个包可以依赖于其他包。SPM 使用 语义化版本控制 (Semantic Versioning) 来管理这些依赖关系,确保项目的稳定性和可预测性。版本号通常为 主版本号.次版本号.修订号
(e.g., 5.6.2
)。
- 主版本号 (Major): 当你做了不兼容的 API 修改。
- 次版本号 (Minor): 当你做了向下兼容的功能性新增。
- 修订号 (Patch): 当你做了向下兼容的问题修正。
SPM 会负责下载和解析整个依赖图(你依赖 A,A 依赖 B,B 依赖 C),并找到一个满足所有包版本限制的解决方案,然后生成一个 Package.resolved
文件来锁定这些版本,确保团队成员和 CI/CD 环境中使用完全相同的依赖版本。
第二章:Xcode 实战——在应用中集成第三方库
对于大多数 iOS/macOS 开发者来说,最常见的场景就是在 Xcode 项目中使用 SPM 来添加第三方库。我们以著名的网络库 Alamofire
为例。
2.1 添加第一个 SPM 依赖
假设你已经创建了一个新的 Xcode 项目(例如,一个 iOS App)。
-
打开添加包界面:在 Xcode 中,选择
File
>Add Packages...
。 -
搜索包:在弹出的窗口右上角的搜索框中,输入
Alamofire
的 Git 仓库 URL:
https://github.com/Alamofire/Alamofire.git
Xcode 会自动去这个地址抓取包的信息。 -
选择依赖规则 (Dependency Rule):
- Up to Next Major Version: 这是最常用也是推荐的选项。例如,如果你指定
5.6.0
,SPM 会接受所有5.x.x
的更新(如5.6.1
,5.7.0
),但不会更新到6.0.0
。这在保证获取最新功能和修复的同时,避免了破坏性的大版本更新。 - Up to Next Minor Version: 更严格的规则。指定
5.6.0
,SPM 会接受5.6.x
的更新(如5.6.1
,5.6.2
),但不会更新到5.7.0
。 - Range: 指定一个明确的版本范围,如
5.6.0..<5.8.0
。 - Exact Version: 锁定一个精确的版本号,如
5.6.2
。这会阻止任何自动更新。 - Branch: 直接跟踪某个开发分支,如
develop
。这通常用于测试尚未发布的特性,不建议在生产项目中使用。 - Commit: 锁定到某一次特定的 Git 提交哈希值。这是最精确的锁定方式。
我们选择
Up to Next Major Version
,并保持默认的最低版本。 - Up to Next Major Version: 这是最常用也是推荐的选项。例如,如果你指定
-
添加包到项目:点击
Add Package
按钮。Xcode 会开始解析依赖。 -
选择产品并添加到目标:解析完成后,Xcode 会显示这个包提供了哪些产品(对于 Alamofire,就是一个名为
Alamofire
的库)。确保它被勾选,并在 “Add to Target” 列中选择你的主 App 目标。然后点击Add Package
。
现在,你会在 Xcode 的项目导航器左侧看到一个新的 “Package Dependencies” 分区,Alamofire
赫然在列。这表示你已成功添加依赖。
2.2 管理依赖
- 更新依赖:要检查并更新所有包到最新版本(在你的版本规则允许的范围内),只需选择
File
>Packages
>Update to Latest Package Versions
。 - 解析依赖:如果
Package.resolved
文件丢失或损坏,或者你想强制 SPM 重新计算依赖关系,可以选择File
>Packages
>Resolve Package Versions
。 - 修改或移除依赖:在项目导航器中,选中你的项目文件,然后进入
Package Dependencies
标签页。在这里,你可以双击某个包来修改其版本规则,或者选中它并点击-
按钮来将其从项目中移除。
2.3 在代码中使用依赖
添加成功后,使用起来非常简单。打开任何一个属于你 App 目标的 Swift 文件(例如 ViewController.swift
),在文件顶部导入模块即可:
“`swift
import UIKit
import Alamofire // 导入 Alamofire 模块
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 现在你可以使用 Alamofire 的功能了
AF.request("https://api.example.com/data").responseJSON { response in
debugPrint(response)
}
}
}
“`
第三章:创建你的第一个 SPM 包
当你发现自己在多个项目中重复编写同样的代码(例如一个网络层、一些工具类或自定义 UI 组件),就是时候将它们封装成一个独立的 SPM 包了。
3.1 为什么需要创建自己的包?
- 代码复用:一次编写,多处使用。
- 模块化:将复杂的项目拆分成多个功能独立的模块,降低耦合度。
- 版本管理:可以清晰地管理和迭代你的共享代码。
- 开源共享:轻松地将你的库分享给社区。
3.2 使用 Xcode 创建一个新包
- 打开 Xcode,选择
File
>New
>Package...
。 - 为你的包选择一个存放位置和名称(例如
MyAwesomeLibrary
)。 - Xcode 会自动为你生成一个标准的包结构:
MyAwesomeLibrary/
├── Package.swift // 清单文件
├── README.md // 说明文档
├── Sources/ // 源代码目录
│ └── MyAwesomeLibrary/ // 与包同名的目标目录
│ └── MyAwesomeLibrary.swift // 默认源文件
└── Tests/ // 测试代码目录
└── MyAwesomeLibraryTests/
└── MyAwesomeLibraryTests.swift // 默认测试文件
3.3 使用命令行创建新包
对于服务端 Swift 或偏好命令行的开发者,SPM 提供了强大的命令行工具。
“`bash
1. 创建一个新目录并进入
mkdir MyAwesomeCLITool && cd MyAwesomeCLITool
2. 初始化一个包
–type library 创建一个库
swift package init –type library
或者,创建一个可执行文件
swift package init –type executable
“`
这将生成与 Xcode 相同的目录结构。
3.4 深入解析 Package.swift
的配置
让我们来看一个更完整的 Package.swift
示例,并逐一解析:
“`swift
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: “MyAwesomeLibrary”, // 包的名称
platforms: [
.macOS(.v12), .iOS(.v15) // 指定支持的最低平台版本
],
products: [
// 对外提供的产品,这里是一个名为 “MyAwesomeLibrary” 的库
.library(
name: “MyAwesomeLibrary”,
targets: [“MyAwesomeLibrary”]),
],
dependencies: [
// 包的依赖,例如依赖了 SwiftyJSON
.package(url: “https://github.com/SwiftyJSON/SwiftyJSON.git”, from: “5.0.0”),
],
targets: [
// 包的内部目标
// 这是一个常规目标,名为 MyAwesomeLibrary
.target(
name: “MyAwesomeLibrary”,
dependencies: [
// 这个目标依赖于 SwiftyJSON
“SwiftyJSON”
]),
// 这是一个测试目标
.testTarget(
name: “MyAwesomeLibraryTests”,
dependencies: [“MyAwesomeLibrary”]), // 测试目标通常依赖于它要测试的目标
]
)
“`
关键点解析:
// swift-tools-version:5.7
:这行注释非常重要,它告诉 SPM 使用哪个版本的工具链来解析这个文件。新功能(如插件、二进制依赖)需要更高的版本。platforms
: 明确声明你的包支持的最低操作系统版本。这对于 API 可用性检查至关重要。如果不指定,SPM 会使用一个非常旧的默认值。products
: 定义了当别人依赖你的包时,能import
什么。targets
参数指定了这个产品是由哪个或哪些目标构成的。dependencies
: 在这里列出你的包需要用到的其他 SPM 包。targets
:dependencies
数组在这里再次出现,但意义不同。这里是指定目标之间的依赖关系。它可以依赖于dependencies
中声明的外部包(通过产品名,如"SwiftyJSON"
),也可以依赖于同一个包内的其他目标(通过目标名)。
第四章:SPM 进阶技巧
掌握了基础之后,让我们探索一些能极大提升开发效率的高级技巧。
4.1 本地包依赖:加速开发与调试
想象一下你正在开发一个 App,同时也在开发它所依赖的一个本地包。传统的流程是:修改包 -> 提交/推送 Git -> 在 App 中更新包版本。这个过程非常繁琐。
SPM 提供了完美的解决方案:本地包依赖。
只需将你的本地包文件夹(例如 MyAwesomeLibrary
)直接从 Finder 拖拽到你的 Xcode App 项目的导航器中,放在项目根目录下。
Xcode 会自动识别它,并将其添加为本地依赖。现在:
* 你可以在同一个 Xcode 窗口中同时编辑 App 代码和包代码。
* 对包的任何修改都会立即在 App 中生效,无需任何提交或更新操作。
* 你可以直接在 App 的调试会话中为包的代码设置断点。
4.2 二进制依赖:集成 XCFramework
有时候,你可能需要分发闭源的库,或者集成一些只提供二进制文件的 SDK。SPM 通过 .binaryTarget
支持了 XCFramework
。
在 Package.swift
中,你可以这样定义一个二进制依赖:
swift
.binaryTarget(
name: "MyClosedSourceSDK",
path: "Frameworks/MyClosedSourceSDK.xcframework" // 本地路径
)
或者,引用一个远程的、带有校验和的二进制文件:
swift
.binaryTarget(
name: "SomeRemoteSDK",
url: "https://example.com/sdk.zip",
checksum: "..." // 压缩包的 SHA256 校验和
)
然后,在你的常规目标中像依赖普通包一样依赖它:
swift
.target(
name: "MyAppLogic",
dependencies: ["MyClosedSourceSDK"]
)
4.3 资源管理:在包中包含图片、数据等文件
你的包可能需要包含非代码资源,如图片、JSON 文件、Core Data 模型、Storyboard 等。
-
在你的目标文件夹(如
Sources/MyAwesomeLibrary/
)下创建一个Resources
文件夹,并将资源文件放进去。 -
在
Package.swift
的目标定义中,通过resources
参数声明这些资源:swift
.target(
name: "MyAwesomeLibrary",
dependencies: [],
resources: [
.process("Resources") // 处理 Resources 文件夹下的所有资源
// 或者 .copy("Resources/data.json") // 只复制特定文件
]
) -
在代码中访问资源:SPM 会为包含资源的目标自动生成一个名为
Bundle.module
的静态属性。你可以用它来安全地访问包内的资源。“`swift
import Foundation// 假设 Resources 文件夹下有一个 image.png
let image = UIImage(named: “image”, in: .module, with: nil)// 假设 Resources 文件夹下有一个 config.json
if let url = Bundle.module.url(forResource: “config”, withExtension: “json”) {
// … 使用 URL 加载数据
}
``
Bundle.module` 是类型安全的,确保你访问的是当前模块的资源包,避免了与主 App 或其他包的资源冲突。
4.4 插件 (Plugins):扩展你的构建流程
SPM 插件允许你在构建过程中运行自定义脚本。这对于代码格式化(如 SwiftFormat)、代码检查(如 SwiftLint)或代码生成等任务非常有用。
- 使用插件:在
Package.swift
的dependencies
中添加插件包,然后在你的目标上应用它。
swift
// In Package.swift
.target(
name: "MyAwesomeLibrary",
dependencies: [],
plugins: [
.plugin(name: "SwiftLintPlugin", package: "SwiftLint")
]
) - 运行插件:在 Xcode 中,你可以右键点击目标,然后选择并运行插件。在命令行中,可以使用
swift package <plugin-name>
。
第五章:命令行中的 SPM
对于服务端 Swift 开发者,或者任何在终端中工作的开发者来说,SPM 的命令行接口是日常工作的核心。
swift package init --type <library|executable>
: 初始化一个新包。swift build
: 编译包。默认是调试构建。swift build -c release
: 进行发布(优化)构建。
swift test
: 运行测试目标。swift run
: 如果包里有可执行产品,用此命令来编译并运行它。swift package update
: 更新依赖到Package.resolved
文件中允许的最新版本。swift package resolve
: 仅解析依赖关系图并生成Package.resolved
,但不进行编译。swift package describe --type json
: 以 JSON 格式输出包的完整描述,对于工具集成非常有用。swift package generate-xcodeproj
: 生成一个 Xcode 项目文件。虽然现在 Xcode 已原生支持直接打开 SPM 包目录,但在某些旧的工作流或 CI 环境中可能仍然需要。
总结:拥抱 SPM,迈向现代化 Swift 开发
Swift Package Manager 已经从一个最初主要服务于服务端 Swift 的工具,成长为 Apple 生态系统中最核心、最强大的依赖管理和构建工具。
SPM 的核心优势在于:
* 官方支持与深度集成:与 Xcode 和 Swift 工具链无缝协作。
* 简洁与类型安全:Package.swift
使用 Swift 编写,易于理解且功能强大。
* 去中心化:任何 Git 仓库都可以是一个包,没有中心仓库的限制。
* 性能与可靠性:高效的依赖解析和缓存机制,以及 Package.resolved
带来的可复现构建。
* 跨平台:一套工具链同时服务于 Apple 平台应用和 Linux 服务端应用。
从今天起,对于你的所有新项目,都应该优先选择 SPM 作为依赖管理方案。对于还在使用 CocoaPods 或 Carthage 的旧项目,也值得花时间评估和迁移到 SPM,以享受更现代化、更流畅的开发体验。掌握 SPM,不仅是掌握了一个工具,更是迈向了更高效、更规范的现代化 Swift 开发之路。