全面了解 Swift Package Manager (SPM)
在现代软件开发中,依赖管理是一个不可或缺的环节。无论是构建一个简单的命令行工具,还是开发一个复杂的跨平台应用程序,我们几乎总是需要利用第三方库或组织内部共享的代码模块。高效、可靠地管理这些依赖关系,决定着开发流程的顺畅程度和项目的可维护性。
对于 Swift 开发者而言,Swift Package Manager (SPM) 正是官方提供的、为 Swift 代码分发而生的依赖管理工具。它不仅简化了在项目中使用外部代码的过程,也为开发者创建、共享和发布自己的代码模块提供了标准化的方式。自 Swift 3 引入以来,SPM 经历了持续的演进和完善,如今已成为 Swift 生态系统中日益重要的组成部分。
本文将带你全面深入地了解 Swift Package Manager,从它的核心概念、优势,到如何在实际开发中应用,以及一些高级特性和最佳实践。
一、 Swift Package Manager (SPM) 是什么?为什么需要它?
1.1 什么是 Swift Package Manager?
Swift Package Manager (SPM) 是一个用于管理 Swift 代码分发的工具。它被深度集成到 Swift 编译器和开发环境中,能够自动化处理代码库(称为“包”或“Package”)的下载、编译、链接等过程。简单来说,SPM 负责解决“我的项目依赖于库 A 和库 B,库 A 又依赖于库 C,如何高效地获取所有这些库并让它们在我的项目中正常工作”的问题。
SPM 的核心是 Package.swift
清单文件,它以 Swift 语言编写,描述了包的名称、目标(Targets,即代码模块)、产品(Products,即可以被其他包或应用使用的产物,如库或可执行文件)、依赖项(Dependencies)以及其他配置信息。
1.2 为什么需要依赖管理工具?
在没有依赖管理工具之前,开发者管理外部代码通常采用以下方式:
- 手动复制代码: 将所需的代码文件直接复制到项目目录中。这种方式在项目初期可能可行,但随着库的更新,手动替换和合并代码会变得极其繁琐且容易出错。
- 使用 Git Submodules: 将外部仓库作为子模块添加到项目中。这解决了版本控制的问题,但子模块的管理相对复杂,特别是处理多级依赖关系时,容易陷入“依赖地狱”(Dependency Hell),即不同库依赖同一个第三方库的不同版本,导致版本冲突。
- 其他第三方工具: 在 SPM 出现之前,CocoaPods 和 Carthage 是 iOS/macOS 开发领域常用的依赖管理工具。它们各自有优缺点,但都不是 Swift 原生且跨平台的解决方案。
这些传统方法存在诸多痛点:
- 管理复杂: 尤其是在处理多级依赖和版本冲突时,手动干预多,效率低下。
- 更新困难: 库有新版本时,升级过程可能涉及复杂的步骤和潜在的破坏性变更。
- 构建集成差: 需要额外的构建步骤或配置才能让外部库与项目一起编译。
- 缺乏标准化: 没有统一的方式来描述和分发 Swift 代码库。
- 跨平台限制: 某些工具可能仅限于特定的平台(如 iOS/macOS)。
Swift Package Manager 的出现正是为了解决这些问题,它为 Swift 代码的分发和依赖管理提供了一个官方、标准化、集成化且跨平台的解决方案。
二、 Swift Package Manager 的核心概念
理解 SPM 的核心概念是掌握其用法的关键。
2.1 包 (Packages)
一个包是一个或多个目标、产品和依赖项的集合。在文件系统层面,一个包通常对应一个 Git 仓库,根目录包含一个 Package.swift
文件。这个 Package.swift
文件是包的入口和定义。
一个包可以包含源代码、资源文件、测试代码以及其他相关文件。
2.2 目标 (Targets)
目标是包的基本构建块。一个目标定义了一个代码模块,它可以是一个库(Library)、一个可执行文件(Executable)、一个测试套件(Test Suite)等。每个目标通常对应于文件系统中的一个目录,该目录包含目标的源代码。
目标之间可以相互依赖。例如,一个测试目标会依赖于它正在测试的库目标。
SPM 定义了以下几种主要的目标类型:
.library(name: ..., dependencies: ...)
: 构建一个库模块,其代码可以被其他目标引用。.executable(name: ..., dependencies: ...)
: 构建一个可执行程序,包含main
函数作为入口。.testTarget(name: ..., dependencies: ...)
: 构建一个测试模块,用于包含测试代码。.binaryTarget(name: ..., path: ...)
或.binaryTarget(name: ..., url: ..., checksum: ...)
: 引用一个预编译的二进制框架(.xcframework
)。
2.3 产品 (Products)
产品是一个包可以向外部提供的可使用单元。一个包可以包含一个或多个产品。产品是目标的一种表现形式。
SPM 定义了以下几种主要的产品类型:
.library(name: ..., targets: ...)
: 这是一个库产品,它可以由包中的一个或多个目标组成。其他包或应用程序可以通过依赖这个库产品来使用其提供的代码。库产品是可导入的模块。.executable(name: ..., targets: ...)
: 这是一个可执行产品,通常对应包中的一个或多个可执行目标。它可以作为独立的程序运行。
注意:一个包定义了目标,然后将一个或多个目标组合成产品,这些产品才是其他包或应用程序能够“看到”并使用的东西。
2.4 依赖项 (Dependencies)
依赖项是你的包所需要的外部包。这些外部包通常托管在 Git 仓库中。在 Package.swift
文件中,你可以声明你的包依赖于哪些其他包,以及需要这些包的哪个版本或哪个分支。
SPM 会负责递归地解析所有依赖项,包括依赖项的依赖项,直到构建出一个完整的依赖图,并下载所有必需的包。
2.5 Package.swift
文件
Package.swift
文件是 Swift 包的清单(Manifest),它使用 Swift 代码定义了包的所有属性。它是 SPM 理解和处理一个包的唯一入口。
一个典型的 Package.swift
文件结构如下:
“`swift
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: “MyAwesomePackage”, // 包的名称
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], // 支持的平台和最低版本 (可选)
products: [
// 定义产品,即这个包向外部提供的可使用单元
.library(
name: “MyAwesomeLibrary”, // 库产品的名称
targets: [“MyAwesomeLibrary”]), // 组成这个库产品的目标列表
.executable(
name: “MyAwesomeCLI”, // 可执行产品的名称
targets: [“MyAwesomeCLI”]) // 组成这个可执行产品的目标列表
],
dependencies: [
// 声明这个包依赖的外部包
.package(url: “https://github.com/apple/swift-argument-parser”, from: “1.0.0”), // 依赖 swift-argument-parser 包,版本从 1.0.0 开始
.package(url: “https://github.com/Alamofire/Alamofire.git”, .upToNextMajor(from: “5.4.0”)), // 依赖 Alamofire 包,版本从 5.4.0 开始,直到下一个主版本更新前
.package(url: “https://github.com/onevcat/Kingfisher.git”, .branch(“main”)) // 依赖 Kingfisher 包的 main 分支
],
targets: [
// 定义目标,即包中的代码模块
.target(
name: “MyAwesomeLibrary”, // 目标名称
dependencies: [ // 这个目标依赖的其他目标或外部包产品
.product(name: “ArgumentParser”, package: “swift-argument-parser”) // 依赖 swift-argument-parser 包中的 ArgumentParser 产品
],
resources: [.process(“Resources”)]), // 包含资源文件
.executableTarget(
name: “MyAwesomeCLI”, // 可执行目标名称
dependencies: [“MyAwesomeLibrary”]), // 依赖 MyAwesomeLibrary 目标
.testTarget(
name: “MyAwesomeLibraryTests”, // 测试目标名称
dependencies: [“MyAwesomeLibrary”]), // 依赖 MyAwesomeLibrary 目标进行测试
]
)
“`
2.6 源代码和资源文件
每个目标通常对应文件系统中的一个源文件目录。SPM 遵循一些约定俗成的目录结构:
- 源文件: 默认情况下,SPM 会在目标目录(例如
Sources/MyAwesomeLibrary
)下查找.swift
文件。 - 资源文件: 资源文件(如图片、本地化字符串文件)可以放在目标目录下的
Resources
子目录中。你需要在Package.swift
中指定如何处理这些资源(例如.process("Resources")
)。 - 头文件: 对于 C/C++/Objective-C 代码,头文件通常放在目标目录下的
include
子目录中。
三、 SPM 的基本使用:在项目中添加依赖
在实际应用中,我们最常使用 SPM 的场景是在已有的 Swift 项目中添加第三方库作为依赖。SPM 被深度集成到 Xcode 中,这使得操作变得非常便捷。
3.1 使用 Xcode GUI 添加依赖
这是在 iOS/macOS 应用开发中最常用的方式。
- 打开项目: 在 Xcode 中打开你的
.xcodeproj
或.xcworkspace
文件。 - 导航到项目设置: 在 Project Navigator (左侧导航栏) 中选中你的项目根节点。
- 选择 Swift Packages 标签页: 在主编辑区域,选择 “Swift Packages” 标签页。
- 点击添加按钮: 点击左下角的
+
按钮。 - 输入仓库 URL: 在弹出的搜索框中输入你想要添加的 Swift 包的 Git 仓库 URL(例如
https://github.com/Alamofire/Alamofire.git
)。Xcode 会连接到 Git 仓库并展示可用的版本规则。 - 选择版本规则: 根据你的需求选择依赖的版本规则。常见的选项包括:
- Up to Next Major Version (推荐用于应用程序): 依赖于指定的起始版本(例如 5.4.0)及后续所有次要版本和补丁版本,直到下一个主版本发布。这样可以在不引入潜在的破坏性变更的情况下获得 bug 修复和新特性。
- Up to Next Minor Version: 依赖于指定的起始版本及后续所有补丁版本,直到下一个次要版本发布。适用于对依赖版本要求更严格的场景,如作为库发布的包。
- Exact Version: 精确锁定到某个特定的版本。通常用于需要极高版本稳定性的场景,或作为临时解决方案。
- Branch: 依赖于仓库的某个特定分支。通常用于开发或测试尚不稳定版本的依赖。
- Commit: 依赖于仓库的某个特定的提交哈希。用于锁定到非常具体的代码状态,但不推荐在生产环境使用,因为缺乏可读性且难以管理。
- 添加到目标: 选择你希望哪些项目目标(Targets,如你的 App Target 或某个 Framework Target)依赖这个包。SPM 会将包的产品(通常是库产品)链接到这些目标中,使你可以在这些目标的源代码中导入并使用它。
- 完成: 点击 “Add Package” 按钮。Xcode 会自动下载、解析依赖关系并构建包。完成后,你会在 Project Navigator 的 “Swift Package Dependencies” 部分看到添加的包。
现在你可以在你选择的目标的 Swift 代码中 import YourPackageModule
来使用这个包提供的功能了。
3.2 使用命令行添加依赖 (适用于纯 Swift 包或 CLI 工具)
对于不使用 Xcode 项目的纯 Swift 包、命令行工具或跨平台项目,可以使用 Swift CLI 命令来管理依赖。
- 打开终端: 导航到你的包或项目的根目录(包含
Package.swift
文件)。 - 编辑
Package.swift
: 使用文本编辑器打开Package.swift
文件。 - 添加依赖项: 在
dependencies
数组中添加新的.package
条目,指定仓库 URL 和版本规则。例如:
swift
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
// 添加其他依赖...
], - 将依赖的产品添加到目标: 在需要使用这个依赖的目标的
dependencies
数组中,添加对该依赖包中某个产品的引用。例如:
swift
targets: [
.executableTarget(
name: "MyCLI",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser") // 引用 swift-argument-parser 包的 ArgumentParser 产品
]),
// 添加其他目标...
] - 解析并获取依赖: 在终端中运行以下命令:
bash
swift package resolve
这个命令会根据Package.swift
文件解析所有依赖,下载它们(如果尚未下载),并更新Package.resolved
文件。 - 构建项目: 运行
swift build
命令来构建你的包或可执行文件。
SPM CLI 命令提供了一套完整的工作流,用于创建、构建、测试和管理 Swift 包。
四、 管理依赖的版本和更新
4.1 版本规则详解
在 Package.swift
或 Xcode 中添加依赖时,选择合适的版本规则至关重要。SPM 遵循 Semantic Versioning (SemVer) 规范(主版本号.次版本号.补丁版本号,例如 1.2.3)。
.exact("1.2.3")
: 精确锁定到版本 1.2.3。.upToNextMinor(from: "1.2.0")
: 允许使用版本 1.2.0 及后续的补丁版本(如 1.2.1, 1.2.5),但不包括 1.3.0 或更高版本。.upToNextMajor(from: "1.2.0")
: 允许使用版本 1.2.0 及后续的所有次要版本和补丁版本(如 1.2.1, 1.3.0, 1.5.8),但不包括 2.0.0 或更高版本。这是推荐用于应用程序依赖的规则。.branch("main")
: 始终使用main
分支上的最新提交。不稳定,不推荐用于生产依赖。.revision("abcdef123456...")
: 锁定到特定的 Git 提交哈希。不推荐日常使用。
4.2 Package.resolved
文件
当你成功添加并解析了依赖后,SPM 会在你的项目根目录(或 .xcworkspace
目录)下生成一个 Package.resolved
文件。这个文件记录了你的项目当前实际使用的每个依赖包的精确版本(或提交哈希)。
Package.resolved
文件的作用是 锁定依赖版本,确保你的项目在不同时间或不同机器上构建时,使用的都是同一组确切版本的依赖。这避免了因为依赖库发布新版本(即使是次要版本或补丁版本)而可能引入的意外行为或构建失败。
重要:Package.resolved
文件应该被提交到版本控制系统(如 Git)中! 这保证了团队成员或 CI/CD 系统都能使用完全相同的依赖环境来构建项目。
4.3 更新依赖
随着时间的推移,你可能需要更新项目使用的依赖库到新版本,以获取 bug 修复、新特性或性能改进。
- 使用 Xcode GUI 更新: 在 Xcode 的 “Swift Packages” 标签页中,选中你想要更新的包,右键点击(或 Control-点击),选择 “Update to Latest Package Versions”。Xcode 会检查所有依赖是否有符合你版本规则的新版本,并更新
Package.resolved
文件。 - 使用命令行更新: 在终端中导航到你的包或项目根目录,运行以下命令:
bash
swift package update
这个命令会检查Package.swift
中声明的所有依赖是否有符合版本规则的新版本,下载新版本,并更新Package.resolved
文件。
4.4 处理依赖冲突
当你的多个依赖项间接依赖同一个第三方库的不同版本时,可能会发生依赖冲突。SPM 具有一定的依赖解析能力,会尝试找到一个兼容所有依赖的版本范围。如果 SPM 无法找到一个兼容的版本,它会报错并告诉你冲突所在。
解决依赖冲突通常需要:
- 查看 SPM 报告的冲突信息,确定是哪个共享依赖引起的问题。
- 检查引起冲突的直接依赖(你的项目直接依赖的包)是否可以使用一个允许更高版本共享依赖的版本。
- 有时候,可能需要手动调整你的某个直接依赖的版本规则,使其允许使用一个与另一个依赖兼容的共享库版本。
- 如果冲突无法通过调整版本规则解决,可能需要等待某个依赖库发布兼容新版本的更新,或者考虑替换其中一个冲突的依赖库。
五、 创建自己的 Swift Package
SPM 不仅是消费依赖的工具,更是创建和分发 Swift 代码模块的标准方式。
5.1 创建一个新包
使用 Swift CLI 命令可以快速创建一个新的 Swift 包:
“`bash
创建一个库包
swift package init –type library MyNewLibrary
创建一个可执行包 (CLI 工具)
swift package init –type executable MyNewCLI
“`
这会在当前目录下创建一个名为 MyNewLibrary
或 MyNewCLI
的文件夹,其中包含基本的目录结构和一个初始的 Package.swift
文件。
5.2 包的目录结构
一个典型的 Swift 包目录结构如下:
MyAwesomePackage/
├── .gitignore
├── Package.swift // 包清单文件
├── README.md
├── Sources/ // 存放目标源代码的目录
│ ├── MyAwesomeLibrary/ // 库目标 'MyAwesomeLibrary' 的源代码目录
│ │ └── MyAwesomeLibrary.swift
│ └── MyAwesomeCLI/ // 可执行目标 'MyAwesomeCLI' 的源代码目录
│ └── main.swift
└── Tests/ // 存放测试目标源代码的目录
└── MyAwesomeLibraryTests/ // 测试目标 'MyAwesomeLibraryTests' 的源代码目录
├── MyAwesomeLibraryTests.swift
└── XCTestManifests.swift // XCTest 框架自动生成的文件
SPM 默认约定:
Sources/
目录下每个子目录通常对应一个目标。Tests/
目录下每个子目录通常对应一个测试目标,其名称约定为对应的源目标名称后加 “Tests”。
5.3 编写 Package.swift
清单文件
当你创建一个包后,需要编辑 Package.swift
文件来定义你的包的属性。
“`swift
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: “MyCustomPackage”, // 包的名称
platforms: [.macOS(.v10_15), .iOS(.v13)], // 可选:指定支持的平台和最低版本
products: [
// 定义你的包向外部提供的产品
.library(
name: “MyCustomLibrary”,
targets: [“MyCustomLibrary”]), // 这个库产品由 MyCustomLibrary 目标组成
.executable(
name: “mytool”,
targets: [“MyCustomCLI”]) // 这个可执行产品由 MyCustomCLI 目标组成
],
dependencies: [
// 定义你的包依赖的外部包
// 例如,如果你的库需要 Alamofire
.package(url: “https://github.com/Alamofire/Alamofire.git”, .upToNextMajor(from: “5.4.0”))
],
targets: [
// 定义包内的目标
.target(
name: “MyCustomLibrary”, // 库目标
dependencies: [
// 如果你的库依赖了外部包的产品,在这里声明
.product(name: “Alamofire”, package: “Alamofire”)
]),
.executableTarget(
name: “MyCustomCLI”, // 可执行目标
dependencies: [“MyCustomLibrary”]), // 可执行目标依赖于你的库目标
.testTarget(
name: “MyCustomPackageTests”, // 测试目标
dependencies: [“MyCustomLibrary”]), // 测试目标依赖于你的库目标
]
)
“`
关键属性解释:
name
: 包的名称,通常与仓库名称一致。platforms
: (可选)指定你的包兼容的最低平台版本。这对于库的消费者很重要。products
: 定义了你的包对外暴露的内容。其他包或应用将依赖这里的name
。dependencies
: 定义了你的包需要哪些外部包。targets
: 定义了包内部的代码模块。targets
内部的dependencies
定义了目标之间的依赖关系,以及目标对外部包产品的依赖。
5.4 添加源代码
在对应的目标目录下添加 .swift
源文件。例如,对于 MyCustomLibrary
目标,在 Sources/MyCustomLibrary/
目录下创建 MyCustomLibrary.swift
:
“`swift
public struct MyCustomLibrary {
public private(set) var text = “Hello, Package!”
public init() {
}
public func sayHello() {
print(text)
}
}
“`
注意:默认情况下,SPM 只会编译 public
和 internal
级别的符号。如果你希望你的代码可以在其他模块中被 import
使用,确保使用 public
访问控制修饰符。
5.5 添加资源和本地化
SPM 支持在包中包含资源文件(图片、音频、JSON 等)和本地化字符串。
- 将资源文件放入目标目录下的
Resources
子目录:
Sources/MyAwesomeLibrary/
├── MyAwesomeLibrary.swift
└── Resources/
├── logo.png
└── strings.txt - 在
Package.swift
中配置目标: 在对应的.target
定义中添加resources
参数。
swift
.target(
name: "MyAwesomeLibrary",
dependencies: [],
resources: [.process("Resources")]), // 指定处理 Resources 目录下的所有文件
.process
表示 SPM 会根据平台和文件类型智能地处理资源(例如,在 macOS 上直接复制,在 iOS 上可能打包进 Asset Catalogs)。.copy
则只是简单地复制文件。 - 在代码中访问资源: 资源会被 SPM 打包到包的 Bundle 中。你可以通过 Bundle 对象访问它们。
swift
// 假设你的资源文件叫 "data.json" 放在 Sources/MyAwesomeLibrary/Resources 目录下
// 资源会打包到 MyAwesomeLibrary 目标对应的 Bundle 中
if let bundle = Bundle.module { // Bundle.module 是 SPM 提供的便捷方式
if let url = bundle.url(forResource: "data", withExtension: "json") {
// 使用 url 加载资源
}
}
对于本地化,你可以在资源目录下创建语言特定的子目录(例如 en.lproj
, zh-Hans.lproj
),并在 Package.swift
中指定 localization
参数:
swift
.target(
name: "MyAwesomeLibrary",
dependencies: [],
resources: [.process("Resources")],
localization: .enabled(
[.pending]) // 或者指定支持的具体语言 [.supported("en"), .supported("zh-Hans")]
),
5.6 编写测试
在 Tests/
目录下对应的测试目标中添加测试代码,使用 XCTest 框架。
“`swift
// Tests/MyAwesomeLibraryTests/MyAwesomeLibraryTests.swift
import XCTest
@testable import MyAwesomeLibrary // 导入被测试的库目标
final class MyAwesomeLibraryTests: XCTestCase {
func testExample() throws {
// XCTest 相容的测试代码
let myLibrary = MyAwesomeLibrary()
XCTAssertEqual(myLibrary.text, “Hello, Package!”)
}
}
“`
5.7 构建和测试包
使用 Swift CLI 命令进行构建和测试:
“`bash
构建包中的所有产品
swift build
运行包中的所有测试
swift test
运行可执行产品 (如果包中有)
swift run [产品名称, 如 MyAwesomeCLI]
“`
5.8 发布包
将你的 Swift 包托管到一个公开可访问的 Git 仓库(如 GitHub、GitLab 等)。遵循 Semantic Versioning 规范,为你的包打上版本标签(tags),例如 1.0.0
。
其他开发者就可以通过你的仓库 URL 和版本标签,在他们的 Package.swift
或 Xcode 中添加对你包的依赖了。
六、 SPM 与 Xcode 的深度集成
SPM 在 Xcode 11 及更高版本中得到了极好的支持,使得在 Apple 平台上使用 SPM 管理依赖成为主流。
6.1 在 Xcode 项目中使用 SPM 包
如前所述,通过 Xcode GUI 添加依赖是主要的集成方式。SPM 包一旦添加到 Xcode 项目中,它就成为了项目的一部分:
- Project Navigator: 在项目导航器中,你会看到一个 “Swift Package Dependencies” 部分,列出了所有添加的包。展开每个包可以看到它的产品。
- Target Dependencies: 在每个目标的 “General” 设置页面的 “Frameworks, Libraries, and Embedded Content” 部分,你会看到 SPM 包的产品被自动添加。这意味着这些产品会被链接到你的目标中,你可以在代码中导入它们。
- Building: 当你构建你的 Xcode 项目时,Xcode 会自动触发 SPM 来解析、下载和构建所有依赖的包。
- Editing: 你可以在 Xcode 中直接打开并编辑 SPM 包的代码(通常需要通过右键菜单或双击包进行操作),这在调试依赖问题时非常有用。Xcode 甚至支持使用本地的包副本进行开发。
6.2 将 Xcode 项目组织为 SPM 包
随着项目的发展,特别是大型项目,将代码库分解为独立的模块(Swift 包)是推荐的做法。这提高了代码的可重用性、可维护性和构建速度。
你可以创建一个或多个本地 Swift 包,并将部分代码或功能迁移到这些包中。然后在你的主 Xcode 项目中,通过文件路径依赖(path: "../MyLocalPackage"
)来引用这些本地包。
这种“内部模块化”策略是利用 SPM 提升项目架构的一个强大方式。
6.3 使用本地包进行开发
当你在开发一个 Swift 包时,你可能希望在一个实际的应用项目中测试这个包。
- 创建本地包: 按照前述步骤创建一个本地 Swift 包。
- 将本地包添加到 Xcode 项目:
- 在 Xcode 的 Project Navigator 中,右键点击你的项目或工作空间。
- 选择 “Add Packages…”。
- 在搜索框下方,选择 “Add Local…” 按钮。
- 导航到你的本地包所在的目录并选择它。
- Xcode 会像处理远程依赖一样处理这个本地包,并让你选择将其添加到哪些目标。
- 开发和测试: 现在你可以在 Xcode 中同时编辑你的应用程序代码和本地包的代码。任何对本地包代码的修改都会立即反映在应用程序的构建中,无需发布到远程仓库。
当你准备好发布本地包时,可以将其推送到远程 Git 仓库,并在应用程序项目中将依赖方式从 path:
改为 url:
和版本规则。
七、 高级话题与最佳实践
7.1 二进制依赖
SPM 支持引用预编译的二进制框架 (.xcframework
)。这对于分发闭源库或包含 Objective-C/C++ 代码的库非常有用。
在 Package.swift
中声明二进制目标:
swift
targets: [
.binaryTarget(
name: "MyClosedSourceLibrary",
url: "https://example.com/binaries/MyClosedSourceLibrary.xcframework.zip",
checksum: "a1b2c3d4e5f6..." // ZIP 文件的校验和,用于验证下载的文件没有被篡改
)
]
或者,如果二进制框架在本地:
swift
targets: [
.binaryTarget(
name: "MyClosedSourceLibrary",
path: "Frameworks/MyClosedSourceLibrary.xcframework" // 相对于 Package.swift 的路径
)
]
然后像使用普通目标一样在产品或依赖中引用这个二进制目标。
7.2 SPM 插件 (Plugins)
Swift 5.6 引入了 SPM 插件功能,允许开发者扩展 SPM 的构建过程。插件可以用 Swift 编写,用于执行各种任务,如代码生成、格式化、 Linting、文档生成等。
例如,你可以创建一个构建工具插件,在编译你的目标之前运行一个代码生成器。或者创建一个命令插件,让你通过 swift package <plugin-name>
命令来执行自定义操作。
插件的定义也在 Package.swift
中,作为一种特殊类型的目标。这是一个更高级的话题,涉及到编写 Swift 脚本和了解 SPM 的构建生命周期。
7.3 跨平台开发
SPM 是 Swift 跨平台战略的核心组成部分。它不仅在 macOS/iOS/tvOS/watchOS 上工作良好,也是在 Linux、Windows 等其他支持 Swift 的平台上构建和管理 Swift 代码的标准方式。
使用 SPM 构建的库和可执行文件天然支持跨平台,前提是你的代码不依赖于特定平台的框架(如 AppKit 或 UIKit)。许多流行的开源 Swift 库(如 Vapor, SwiftNIO, ArgumentParser)都使用 SPM 发布,并且支持 Linux 等平台。
7.4 模块化与内部包
正如前面提到的,即使是大型的单体应用项目,也可以受益于使用 SPM 将代码分解为多个内部库包。
好处:
- 更快的构建速度: SPM 会缓存已构建的包,如果包的代码没有改变,下次构建时可以直接使用缓存结果,显著加快大型项目的增量构建速度。
- 更好的代码组织: 强制将相关代码组织在独立的模块中,边界清晰。
- 增强代码重用: 内部包可以更容易地在项目的不同部分甚至其他项目之间共享。
- 更易于测试: 可以为每个内部包编写独立的单元测试套件。
7.5 依赖图和缓存
SPM 在内部维护一个依赖图,记录了项目及其所有依赖项之间的关系。当执行 swift package resolve
或 swift package update
时,SPM 会遍历这个图来确定需要哪些包及其版本。
SPM 会将下载的依赖包缓存到用户主目录下的一个标准位置(例如 ~/.swiftpm
),避免重复下载。构建产物也会被缓存,以加速后续构建。
7.6 版本锁定策略
在 Package.swift
中使用 .upToNextMajor
或 .upToNextMinor
是声明你的意图——你愿意接受从某个版本开始的哪些更新。而 Package.resolved
文件记录的是现实——你的项目当前锁定到的精确版本。
- 对于应用程序:通常推荐使用
.upToNextMajor
作为版本规则,并始终将Package.resolved
文件提交到版本控制。这为你提供了接收 bug 修复和次要特性更新的灵活性,同时避免了破坏性变更,并通过Package.resolved
保证了构建的一致性。 - 对于库:如果你正在构建一个供其他开发者使用的库,更保守的版本规则
.upToNextMinor
可能更合适。这减少了你的库因为依赖更新而意外地引入 ABI 兼容性问题的风险。同时,你的库通常不应该将Package.resolved
文件提交到版本控制,而是让使用你库的应用程序或另一个库来决定并管理所有依赖(包括你的库的依赖)的最终版本。
八、 SPM 与 CocoaPods/Carthage 的比较
虽然 CocoaPods 和 Carthage 在 SPM 出现之前一直是主流,但 SPM 作为官方原生工具,具有明显的优势:
- 原生集成: 深度集成到 Swift 工具链和 Xcode 中,无需安装额外的 gem 或 brew 包,无需运行独立的安装命令(如
pod install
),无需额外的构建 phases 或生成.xcworkspace
文件。 - 简单性: 配置完全基于 Swift 代码的
Package.swift
文件,比 Podfile (Ruby) 或 Cartfile 简单直观。 - 跨平台: 设计之初就考虑了跨平台支持,而 CocoaPods/Carthage 主要面向 Apple 平台。
- 更快的解析和构建 (通常): SPM 的依赖解析和构建过程通常比 CocoaPods 更快,部分原因在于其更优化的缓存策略和原生集成。
- 去中心化: SPM 可以直接从 Git 仓库下载包,不需要中心化的Specs仓库(如 CocoaPods 需要)。虽然也有 Package Index 这样的发现机制,但核心机制是去中心化的。
不过,CocoaPods 和 Carthage 也有其优点,例如更长的历史和更成熟的生态系统(特别是 CocoaPods 拥有庞大的现有库集合),以及对 Objective-C/C++ 混合项目的更好支持(SPM 对二进制依赖的支持正在改进,但对于纯源码的混合语言包支持不如 CocoaPods)。
然而,随着 SPM 的不断发展和完善,越来越多的库开始支持 SPM,并且它是未来 Swift 依赖管理的明确方向。对于新的 Swift 项目,首选 SPM 通常是最佳选择。
九、 SPM 的未来
Swift Package Manager 仍在积极开发中。未来的版本可能会带来更多改进:
- 更强的构建系统集成: 与 Swift 构建系统更深度的结合。
- 更多的插件能力: 扩展插件的功能和类型。
- 资源和本地化的增强: 更精细的资源处理和本地化支持。
- 依赖解析和冲突解决的优化: 提升在复杂依赖图中的表现。
- 图形化工具和界面: 进一步完善 Xcode 等 IDE 中的 SPM 用户体验。
SPM 已经成为 Swift 开发不可或缺的一部分,它的成熟和普及正在极大地改善 Swift 生态系统的协作和代码共享方式。
十、 结论
Swift Package Manager 为 Swift 代码的分发和依赖管理提供了一个强大、灵活且原生的解决方案。从简单的依赖消费到复杂的包创建和内部模块化,SPM 涵盖了现代软件开发中依赖管理的方方面面。
通过 Package.swift
清单文件,开发者能够清晰地定义代码模块、产品和依赖关系。与 Xcode 的深度集成使得在 Apple 平台上使用 SPM 变得极其便捷。跨平台支持则让 SPM 成为构建跨平台 Swift 应用和库的基石。
无论是新手还是经验丰富的 Swift 开发者,全面了解并熟练运用 Swift Package Manager 都是提升开发效率、构建高质量、可维护软件的必由之路。现在就开始在你的项目中使用 SPM 吧!