Swift Package Manager (SPM) 快速入门教程:从概念到实践
引言:拥抱 Swift 包管理的新时代
在现代软件开发中,依赖管理是一个至关重要的环节。它帮助开发者组织、共享和复用代码模块(即“包”或“库”)。对于 Swift 生态系统而言,Apple 推出的 Swift Package Manager (SPM) 扮演着官方、跨平台的包管理工具角色。SPM 不仅让依赖管理变得更加 Swift 原生,而且与 Xcode 紧密集成,极大地简化了项目的构建和分发流程。
本教程将带你从零开始,详细了解 SPM 的核心概念、基本用法,包括如何创建 Swift 包、如何添加依赖、如何在项目中使用包,以及如何在命令行和 Xcode 中进行操作。无论你是 Swift 开发新手,还是从其他包管理器(如 CocoaPods 或 Carthage)迁移过来,本教程都将为你提供一个快速掌握 SPM 的全面指南。
我们将涵盖以下主要内容:
- 什么是 Swift Package Manager (SPM)? – 理解其目的、优势及在 Swift 生态中的地位。
- SPM 的核心概念 – 包、产品、目标、依赖等。
- 创建一个 Swift 包 – 使用命令行工具初始化不同类型的包。
Package.swift
解析 – 理解包清单文件的结构和作用。- 添加和管理依赖 – 如何在
Package.swift
中声明外部库,并理解版本控制。 - 在 Swift 项目中使用 SPM – 命令行构建、运行与测试。
- SPM 与 Xcode 的集成 – 在 Xcode 项目中添加和使用 Swift 包。
- 关键概念深挖 – 目标与产品、依赖解析等。
- 常用 SPM 命令详解。
- 最佳实践与技巧。
准备好了吗?让我们开始 Swift 包管理的旅程!
1. 什么是 Swift Package Manager (SPM)?
Swift Package Manager 是一个用于自动化分发 Swift 代码并将其集成到你的构建系统中的工具。它由 Apple 开发并维护,并且是开源的。SPM 的目标是让 Swift 代码的共享和复用变得简单、高效且跨平台(支持 macOS, Linux, iOS, tvOS, watchOS 等)。
SPM 的主要优势:
- Swift 原生: 使用 Swift 语言编写的包清单文件 (
Package.swift
),与其他 Swift 代码风格一致。 - 跨平台: 可以在任何支持 Swift 的平台上使用 SPM。
- 与 Swift 工具链集成: 作为 Swift 工具链的一部分发布,无需额外安装(通常随 Swift 或 Xcode 一起安装)。
- 与 Xcode 深度集成: Xcode 原生支持 SPM,提供图形界面管理依赖,简化开发流程。
- 去中心化: 不需要中心仓库,可以直接引用 Git 仓库的 URL。
- 依赖解析强大: 能够处理复杂的依赖关系图,并自动解决版本冲突。
- 支持二进制依赖: 除了源代码,SPM 也支持引入二进制框架 (
.xcframework
)。
简单来说,SPM 就是 Swift 世界里的依赖管理工具,类似于前端的 npm/yarn,Ruby 的 Bundler,Java 的 Maven/Gradle 等。
2. SPM 的核心概念
理解 SPM 的几个核心概念对于入门至关重要:
- 包 (Package): SPM 的基本组织单元。一个包是一个包含一个或多个目标(Target)、产品(Product)和依赖项的目录。通常,一个 Git 仓库就是一个包。包的根目录下必须有一个
Package.swift
文件。 - 产品 (Product): 包暴露给外部使用的可构建的输出。产品有两种主要类型:
- 库 (Library): 可被其他 Swift 包或应用导入并使用的代码模块。
- 可执行文件 (Executable): 可以直接运行的程序。
- 目标 (Target): 包中的一个构建模块。一个目标定义了其源文件所在的目录、对其他目标或外部包的依赖。一个包可以包含多个目标,通常分为以下几种:
- 普通目标 (Regular Target): 包含库或可执行文件的源代码。
- 测试目标 (Test Target): 包含用于测试其他目标的单元测试或集成测试代码。
- 可执行目标 (Executable Target): 包含一个
main.swift
文件或带有@main
属性的类型,可以构建成一个可执行文件。
- 依赖 (Dependency): 一个包所依赖的其他 Swift 包。SPM 会负责下载这些依赖包的源代码,并在构建时将其集成到你的项目中。
3. 创建一个 Swift 包 (命令行)
SPM 最直接的使用方式是通过命令行工具 swift package
。让我们创建一个新的 Swift 包。
打开终端应用,导航到你想要创建包的目录。
创建一个库包:
如果你想创建一个可以被其他项目引用的库,使用 init
命令并指定类型为 library
(这是默认类型,可以省略):
bash
mkdir MyAwesomeLibrary
cd MyAwesomeLibrary
swift package init --type library
执行完毕后,SPM 会生成以下文件和目录结构:
.
├── Sources/
│ └── MyAwesomeLibrary.swift
├── Tests/
│ ├── MyAwesomeLibraryTests/
│ │ └── MyAwesomeLibraryTests.swift
│ └── XCTestsManifests.swift
├── .gitignore
└── Package.swift
Package.swift
: 包的清单文件,描述了包的名称、目标、产品和依赖。Sources/
: 存放库的源代码文件。默认生成了一个与包同名的.swift
文件。Tests/
: 存放测试代码。默认生成了一个测试目标目录和测试文件。.gitignore
: 包含 SPM 生成的忽略项(如.build
目录)。
创建一个可执行包:
如果你想创建一个可以直接运行的命令行工具,使用 init
命令并指定类型为 executable
:
bash
mkdir MyCommandLineTool
cd MyCommandLineTool
swift package init --type executable
生成的结构略有不同:
.
├── Sources/
│ └── MyCommandLineTool/
│ └── main.swift
├── Tests/
│ └── MyCommandLineToolTests/
│ └── MyCommandLineToolTests.swift
├── .gitignore
└── Package.swift
Sources/MyCommandLineTool/main.swift
: 可执行程序的入口点。- 注意,对于可执行包,源文件通常放在一个与包同名的子目录中,这个子目录就是可执行目标。
文件组织约定:
SPM 遵循约定大于配置的原则。默认情况下:
- 源文件位于
Sources/
目录下。 - 测试文件位于
Tests/
目录下。 - 一个目录下的所有
.swift
文件通常组成一个目标。 - 如果
Sources/
下只有一个目录(如Sources/MyLibrary/
),则该目录下的所有源文件组成一个名为MyLibrary
的目标。 - 如果
Sources/
下有多个目录(如Sources/ModuleA/
,Sources/ModuleB/
),则每个目录对应一个同名目标。
这些约定可以在 Package.swift
中修改,但遵循约定会让事情变得更简单。
4. Package.swift
解析
Package.swift
是 SPM 包的核心。它是一个 Swift 文件,使用 Swift 语言来定义包的结构和属性。让我们看看一个典型的 Package.swift
文件(以库包为例):
“`swift
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: “MyAwesomeLibrary”, // 包的名称
platforms: [
// 支持的平台和最低版本,可选。如果未指定,默认支持所有平台,最低版本由 Swift 工具链决定。
// .macOS(.v10_15),
// .iOS(.v13),
// .tvOS(.v13),
// .watchOS(.v6),
// .visionOS(.v1)
],
products: [
// 定义包提供的产品
.library(
name: “MyAwesomeLibrary”, // 产品名称
targets: [“MyAwesomeLibrary”] // 产品包含的目标
),
],
dependencies: [
// 声明包的依赖项
// .package(url: “https://github.com/apple/swift-argument-parser”, from: “1.2.0”),
],
targets: [
// 定义包中的目标
.target(
name: “MyAwesomeLibrary”, // 目标名称
dependencies: [
// 目标依赖于包内的其他目标或包外的依赖项
// .product(name: “ArgumentParser”, package: “swift-argument-parser”), // 依赖于 swift-argument-parser 包的 ArgumentParser 产品
]
),
.testTarget(
name: “MyAwesomeLibraryTests”, // 测试目标名称
dependencies: [
“MyAwesomeLibrary”, // 测试目标依赖于其测试的普通目标
// 其他测试所需的依赖
]
),
]
)
“`
Package.swift
的关键部分:
// swift-tools-version: 5.9
: 这一行非常重要,它指定了构建该包所需的 Swift 工具链版本。SPM 会根据这个版本来解析Package.swift
文件。如果你的 Swift 版本低于此要求,构建将失败。import PackageDescription
: 导入 SPM 定义的 DSL (Domain Specific Language) 模块,用于构建Package
对象。let package = Package(...)
: 创建并配置一个Package
实例。name
: 包的唯一名称。platforms
: (可选)声明该包支持的最低平台版本。这有助于依赖该包的项目确定兼容性。products
: 定义包暴露给外部使用的产品。一个产品通常对应一个库或一个可执行文件。.library(name:targets:)
: 定义一个库产品。name
是库的名称,targets
是构建该库所需的(一个或多个)目标。.executable(name:targets:)
: 定义一个可执行产品。name
是可执行文件的名称,targets
是包含入口点(如main.swift
)的目标。
dependencies
: 声明该包所依赖的外部 Swift 包。这是添加第三方库的地方。后面会详细讲解依赖的声明方式。targets
: 定义包内部的构建模块。每个目标对应源文件目录,并可以声明其内部依赖。.target(name:dependencies:path:sources:resources:plugins:settings:checksum:)
: 定义一个普通目标(库或可执行目标的组成部分)。name
: 目标名称,通常与源文件目录名相同。dependencies
: (可选)该目标依赖的其他目标或外部包的产品。path
: (可选)指定源文件目录,默认是Sources/
下与目标同名的目录。sources
: (可选)指定该目标包含的具体源文件或排除的文件。resources
: (可选)指定需要包含在构建产物中的资源文件(图片、数据等)。settings
: (可选)编译器设置。
.executableTarget(...)
: 定义一个可执行目标,通常包含main.swift
。是.target
的一个快捷方式,用于表明这是一个可执行的入口。.testTarget(name:dependencies:path:sources:resources:settings:)
: 定义一个测试目标。dependencies
中通常包含要测试的普通目标。
5. 添加和管理依赖
SPM 允许你轻松地将其他 Swift 包作为依赖添加到你的项目中。依赖通常是托管在 Git 仓库中的 Swift 包。
在你的 Package.swift
文件中,找到 dependencies
数组,并添加你的依赖。添加依赖的标准格式是 .package(url: ..., from: ...)
或其他版本控制方式。
常见的依赖声明方式:
- 基于语义版本控制 (Semantic Versioning – SemVer): 这是最推荐的方式。指定一个最低版本,SPM 会自动使用该版本或任何后续的兼容版本(根据 SemVer 规则)。
.package(url: "https://github.com/vendor/package.git", from: "1.2.0")
: 依赖版本1.2.0
或更高,直到下一个主要版本发布前 (< 2.0.0
)。这是最常用且安全的选项,因为它允许获取补丁和次要更新,同时避免不兼容的主要版本更改。.package(url: "...", .upToNextMinor(from: "1.2.0"))
: 依赖版本1.2.0
或更高,直到下一个次要版本发布前 (< 1.3.0
)。.package(url: "...", .exact("1.2.3"))
: 只依赖精确的1.2.3
版本。不推荐,除非有特殊需求,因为它会阻止获取任何更新。.package(url: "...", "1.2.0"..<"1.3.0")
: 依赖1.2.0
到1.3.0
之间的版本(不包含1.3.0
)。
- 基于分支 (Branch): 依赖特定分支的最新提交。
.package(url: "https://github.com/vendor/package.git", branch: "develop")
: 依赖develop
分支的最新代码。适合开发中的依赖,但不适合发布,因为代码可能不稳定。
- 基于提交 (Revision): 依赖 Git 仓库的特定提交(commit hash)。
.package(url: "https://github.com/vendor/package.git", revision: "abcdef1234567890abcdef1234567890")
: 依赖精确的某个提交。极少使用,通常用于锁定一个已知有问题的版本或尚未打标签的版本。
示例:添加一个常用库
假设我们想添加 Swift Argument Parser 这个库来构建命令行工具。它的 GitHub 地址是 https://github.com/apple/swift-argument-parser
。查阅其发布版本,发现最新稳定版是 1.2.3
。
在你的可执行包的 Package.swift
中,修改 dependencies
数组:
swift
dependencies: [
// 声明包的依赖项
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"), // 依赖版本 1.2.0 或更高,直到 < 2.0.0
],
然后,找到你的可执行目标(例如 MyCommandLineTool
),在它的 dependencies
数组中声明对该外部包产品的依赖:
swift
targets: [
.executableTarget(
name: "MyCommandLineTool",
dependencies: [
// 依赖于 swift-argument-parser 包的 ArgumentParser 产品
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
),
// ... 其他目标(如测试目标)
]
依赖解析与 Package.resolved
:
当你添加或修改依赖后,SPM 需要执行一个“依赖解析”过程。这个过程会计算出所有依赖包的精确版本,并确保它们之间没有冲突。解析结果会写入一个 Package.resolved
文件(通常在 .build/
或与 Package.swift
同级,具体位置取决于 SPM 版本和使用方式,Xcode 会放在 .xcodeproj/project.xcworkspace/xcshareddata/SwiftPackageManager/
)。
Package.resolved
文件记录了每个依赖包被解析到的精确版本或提交。 提交并分享 Package.resolved
文件是强烈推荐的,因为它保证了团队成员或在不同时间构建项目时,都能使用完全相同的依赖版本,从而实现可重复构建。
6. 在 Swift 项目中使用 SPM (命令行)
在命令行环境中,SPM 提供了一系列命令来构建、运行和测试你的包。
导航到包含 Package.swift
文件的包根目录。
-
下载和解析依赖:
bash
swift package resolve
这个命令会根据Package.swift
文件下载或更新依赖,并生成/更新Package.resolved
文件。通常在swift build
或swift run
之前隐式调用。 -
构建包:
bash
swift build
这个命令会编译你的包及其所有依赖。构建产物会放在.build/
目录下。对于库包,会生成库文件;对于可执行包,会生成可执行文件。- 可以通过
--configuration release
或--configuration debug
指定构建配置。
- 可以通过
-
运行可执行包:
bash
swift run [可执行产品名称]
如果你创建的是一个可执行包,可以使用swift run
直接运行它。如果包只有一个可执行产品,可以省略产品名称。例如,对于上面的MyCommandLineTool
包:bash
swift run MyCommandLineTool
这个命令会先执行构建(如果需要),然后运行指定的可执行产品。 -
运行测试:
bash
swift test
这个命令会构建并运行包中的所有测试目标。它会执行Tests/
目录下测试目标中的所有测试方法,并输出测试结果。 -
清除构建产物:
bash
swift package clean
删除.build/
目录,清除所有构建生成的文件。在遇到奇怪的构建问题时,执行此命令有时会有帮助。 -
更新依赖:
bash
swift package update
根据Package.swift
中定义的规则(例如.from: "1.2.0"
),检查依赖是否有新版本可用,并更新到满足条件的最新的兼容版本。这会更新Package.resolved
文件。 -
生成 Xcode 项目文件:
bash
swift package generate-xcodeproj
(注意:这个命令在现代 SPM 和 Xcode 版本中已不再推荐或必要。 Xcode 已经原生支持直接打开 SPM 包目录或向现有 Xcode 项目添加 SPM 包,无需生成单独的.xcodeproj
文件。)
在早期 SPM 版本中,此命令用于为包生成一个临时的 Xcode 项目文件,方便在 Xcode 中查看和编辑代码。现在,你可以直接在 Xcode 中打开包含Package.swift
的目录。
7. SPM 与 Xcode 的集成
现代版本的 Xcode 对 SPM 提供了深度原生支持,这使得在 iOS、macOS 等项目中使用 Swift 包变得异常便捷。
方法一:创建一个新的 Xcode 项目并集成 SPM 包
当你创建新的项目时 (File > New > Project),Xcode 提供了一个模板叫做 “Swift Package”。使用这个模板创建的项目本质上就是一个 Swift 包,Xcode 会自动识别 Package.swift
文件,并提供图形界面来管理依赖、构建和运行。
方法二:向现有 Xcode 项目添加 Swift 包依赖
这是最常见的用法。你可以在现有的 iOS、macOS 等应用或框架项目中添加 Swift 包作为依赖。
- 打开你的 Xcode 项目 (
.xcodeproj
或.xcworkspace
)。 - 在 Xcode 菜单栏选择
File > Add Packages...
。 - 在弹出的搜索框中,输入你要添加的 Swift 包的 Git 仓库 URL。你可以直接粘贴 GitHub/GitLab/Bitbucket 等仓库的 URL。
- Xcode 会自动检测并显示该仓库中的 Swift 包。
- 在右侧的 “Dependency Rule” 下拉菜单中,选择你想要的依赖版本规则。通常推荐使用 “Up to Next Major Version”。你也可以选择特定的版本、分支或提交。
- 点击 “Add Package” 按钮。
- Xcode 会开始下载和解析依赖。完成后,弹出一个窗口让你选择将该包的产品(Library 或 Executable)添加到你的哪个目标 (Target)。例如,一个 iOS 应用项目可能有一个与应用同名的主目标和一个测试目标,你可以选择将库产品添加到主目标和测试目标中。
- 选择好目标后,点击 “Add to Targets”。
添加完成后,你会看到:
- 在项目导航器 (Project Navigator) 的项目文件 (.xcodeproj) 下,新增了一个 “Package Dependencies” 部分,列出了你添加的所有 Swift 包。
- 当你选中你的项目文件,然后选中一个具体的目标 (Target),在 “General” 或 “Build Phases” 选项卡中,你会看到添加的 Swift 包产品已经被自动添加到 “Frameworks, Libraries, and Embedded Content” 或 “Link Binary With Libraries” 中。
- Xcode 会自动生成并更新
Package.resolved
文件,它存储在你的.xcodeproj
或.xcworkspace
目录中(具体路径可能是.xcodeproj/project.xcworkspace/xcshareddata/SwiftPackageManager/Package.resolved
)。务必将这个文件提交到你的版本控制系统!
现在,你就可以在你的项目中导入并使用这个 Swift 包提供的代码了,就像使用任何其他模块一样:
“`swift
import ArgumentParser // 如果你添加了 swift-argument-parser
// 使用 ArgumentParser 提供的功能
// …
“`
Xcode 会负责调用 SPM 工具链进行依赖的下载、构建和链接,整个过程对开发者来说非常顺畅。构建你的 Xcode 项目时,SPM 依赖也会被自动构建。
8. 关键概念深挖
为了更好地利用 SPM,我们深入理解几个关键点:
目标 (Target) 与产品 (Product) 的区别与联系:
- 目标 (Target) 是包的内部构建单元。它定义了哪些源文件属于它,以及它依赖包内的哪些其他目标或包外的哪些产品。目标本身通常不对外可见。例如,一个复杂的库可能由多个内部目标组成,每个目标处理一部分功能。
- 产品 (Product) 是包暴露给外部使用的接口。它指定了由哪些目标组合而成,形成一个可导入的库或可运行的程序。其他包或应用依赖的是一个包的产品,而不是它的内部目标。
- 关系: 一个产品由一个或多个目标构建而成。例如,一个库产品
.library(name: "MyLib", targets: ["ModuleA", "ModuleB"])
表示MyLib
库是由ModuleA
和ModuleB
这两个目标编译链接而成的。
依赖解析过程:
当 swift package resolve
或 swift build
/swift run
被执行时,SPM 会进行依赖解析。
- 它读取你的
Package.swift
文件,获取你的直接依赖列表及其版本规则。 - 它获取每个直接依赖的
Package.swift
文件,找出它们的依赖列表。 - 这个过程会递归进行,构建一个完整的依赖关系图,包含所有直接和间接的依赖。
- SPM 会尝试为图中的每个包找到一个满足所有依赖规则的最新版本。如果同一个包被多个依赖以不同的版本规则引用,SPM 会尝试找到一个兼容所有规则的版本。如果找不到兼容版本,依赖解析将失败,并报告冲突。
- 成功的解析结果会写入
Package.resolved
文件。
理解这个过程有助于在遇到依赖冲突时进行排查。
资源文件 (Resources
):
SPM 允许你将非代码资源文件(如图片、JSON、文本文件等)包含在包中。在 .target
或 .library
声明中,可以使用 resources
参数:
swift
.target(
name: "MyTargetWithResources",
dependencies: [],
resources: [.process("Resources")] // 处理 Sources/MyTargetWithResources/Resources 目录下的所有文件
)
.process("Resources")
表示将 Sources/MyTargetWithResources/Resources/
目录下的所有文件添加到构建产物中。这些资源在运行时可以通过 Bundle.module
来访问:
swift
// 假设 Resources 目录下有个 data.json 文件
let url = Bundle.module.url(forResource: "data", withExtension: "json")
这提供了一个标准化的方式来打包和访问与代码相关的资源。
9. 常用 SPM 命令详解
除了上面提到的,还有一些其他有用的命令:
swift package describe [--type json]
:打印包的结构描述,包括产品、目标、依赖等信息。可以输出为 JSON 格式。swift package edit <package-name> [--branch <branch-name>] [--path <path>]
: 将一个依赖包置于“编辑模式”。SPM 会克隆该依赖的 Git 仓库到本地指定路径(默认为SourcePackages/checkouts/
),并让你的项目直接使用本地的代码,而不是缓存的版本。这在你需要修改依赖包代码或调试时非常有用。完成后使用swift package unedit <package-name>
退出编辑模式。swift package show-dependencies [--flatten]
: 显示包的依赖树。--flatten
参数可以将依赖列表展平。swift package completion-tool
: 用于生成命令行补全脚本。
可以通过 swift package --help
查看所有可用命令及其选项。
10. 最佳实践与技巧
- 提交
Package.resolved
文件: 这是保证构建可重复性的关键。 - 使用语义版本控制: 对于你发布的包,遵循 SemVer 规范(
MAJOR.MINOR.PATCH
)。正确使用版本规则(尤其是.from:
或.upToNextMajor
)对于依赖你的开发者非常重要。 - 模块化你的代码: 将大的功能拆分成多个目标,有助于提高代码的可维护性和重用性。
- 编写测试: 为你的包编写全面的测试(在
Tests/
目录下)是一个好习惯。swift test
命令可以方便地运行这些测试。 - 理解
swift-tools-version
: 确保你的Package.swift
中的版本与你使用的 Swift 工具链兼容。如果更新 Swift 版本,有时也需要更新swift-tools-version
。 - 本地路径依赖: 在开发阶段,你可以使用
.package(path: "../LocalPackage")
的方式依赖本地文件系统中的另一个包。这对于同时开发多个相互依赖的包非常有用。但在发布或提交代码时,通常不应包含这种本地路径依赖。
结论:掌握 SPM,提升开发效率
通过本教程,你应该对 Swift Package Manager 有了一个全面且详细的了解。从创建包、理解 Package.swift
文件,到添加依赖、在命令行和 Xcode 中使用 SPM,我们涵盖了快速入门所需的核心知识和实践步骤。
SPM 是 Swift 生态系统中一个强大而便捷的工具。掌握它,你将能够:
- 更有效地组织和管理你的 Swift 代码。
- 轻松地引入和使用第三方开源库。
- 方便地创建和分发你自己的 Swift 代码模块。
- 享受与 Xcode 无缝集成的开发体验。
依赖管理是现代软件开发中不可或缺的一环。拥抱 SPM,利用其自动化和标准化的能力,将极大地提升你的 Swift 开发效率和项目质量。
现在,就开始创建你的第一个 Swift 包,或者将 SPM 依赖添加到你的现有项目中吧!祝你开发顺利!