iOS/macOS 开发:Swift Package Manager (SPM) 入门


iOS/macOS 开发:Swift Package Manager (SPM) 入门详解

在现代软件开发中,代码复用和模块化是提高效率和维护性的关键。无论是构建复杂的 iOS/macOS 应用程序还是可重用的库,我们都离不开依赖管理工具。在 Apple 生态系统中,Swift Package Manager (SPM) 已逐渐成为官方推荐和社区主流的依赖管理解决方案。本文将深入探讨 SPM 的核心概念、使用方法、优势以及与 Xcode 的集成,帮助你全面掌握这个强大的工具。

一、什么是 Swift Package Manager (SPM)?

Swift Package Manager (SPM) 是 Apple 官方推出的一款用于管理 Swift 代码分发和依赖的工具。它于 Swift 3.0 正式引入,旨在简化 Swift 库和可执行文件的创建、共享和使用过程。与 CocoaPods 和 Carthage 等第三方依赖管理工具相比,SPM 的主要优势在于:

  1. 官方支持与深度集成: SPM 由 Apple 开发和维护,与 Swift 语言、编译器以及 Xcode 开发环境深度集成,提供了无缝的开发体验。
  2. 去中心化: SPM 直接从 Git 仓库(或其他支持的版本控制系统)获取代码,无需中心化的索引库(如 CocoaPods 的 Specs repo)。这使得发布和获取包更加直接和灵活。
  3. 基于文件系统约定: SPM 依赖于特定的目录结构和 Package.swift 清单文件来定义包的结构和依赖关系,配置相对直观。
  4. 跨平台潜力: SPM 不仅限于 Apple 平台,也支持 Linux,是 Swift 服务端开发和跨平台工具链的重要组成部分。

对于 iOS 和 macOS 开发者而言,自 Xcode 11 起,SPM 的集成得到了显著增强,可以直接在 Xcode UI 中方便地添加和管理 SPM 依赖,使其成为管理项目第三方库的首选方式之一。

二、SPM 核心概念

理解 SPM 的工作原理,首先需要掌握几个核心概念:

  1. 包 (Package):

    • 一个包是包含 Swift 源代码、资源文件和一个清单文件 (Package.swift) 的集合。
    • 它可以定义一个或多个目标 (Target),这些目标最终可以编译成产品 (Product)(库或可执行文件)。
    • 包可以依赖于其他包。
    • 通常,一个 Git 仓库对应一个包。
  2. 清单文件 (Package.swift):

    • 这是 SPM 的核心配置文件,使用 Swift 语言本身编写。
    • 它描述了包的名称、包含的目标、产品、平台支持以及其依赖关系。
    • 通过 PackageDescription 模块提供的 API 来定义包的结构。
  3. 目标 (Target):

    • 目标是包内可独立构建的单元,通常对应 Xcode 中的一个 Target。
    • 主要有两种类型的目标:
      • 常规目标 (.target): 包含源代码和资源,编译成模块(库的一部分)。可以依赖其他目标(同一包内或来自依赖包)。
      • 测试目标 (.testTarget): 包含单元测试或集成测试代码,用于测试某个常规目标。它依赖于对应的常规目标。
      • 二进制目标 (.binaryTarget): 引用预编译好的二进制库 (.xcframework),用于分发闭源库或加速构建。
  4. 产品 (Product):

    • 产品是包构建后最终产出的可交付成果,定义了哪些目标应该被外部使用。
    • 主要类型:
      • 库 (.library): 将一个或多个目标打包成模块,供其他包或应用程序使用。可以是静态库或动态库。
      • 可执行文件 (.executable): 将一个或多个目标编译成可在特定平台上运行的命令行工具或程序。
  5. 依赖 (Dependency):

    • 指一个包需要使用的其他包。
    • Package.swift 中声明,需要指定依赖包的来源(通常是 Git URL)和版本要求。
    • SPM 负责解析、下载和构建这些依赖项及其传递依赖项(依赖的依赖)。

三、SPM 的基本使用

1. 创建一个 Swift 包

如果你想创建一个可复用的库或命令行工具,可以使用 SPM 来初始化项目结构。

打开终端,执行以下命令:

```bash

创建一个名为 MyLibrary 的库包

mkdir MyLibrary
cd MyLibrary
swift package init --type library

或者创建一个名为 MyExecutable 的可执行包

mkdir MyExecutable
cd MyExecutable
swift package init --type executable
```

这会生成基本的目录结构和 Package.swift 文件:

  • 库包结构:
    MyLibrary/
    ├── Sources/
    │ └── MyLibrary/
    │ └── MyLibrary.swift # 源代码
    ├── Tests/
    │ └── MyLibraryTests/
    │ └── MyLibraryTests.swift # 测试代码
    └── Package.swift # 清单文件
  • 可执行包结构:
    MyExecutable/
    ├── Sources/
    │ └── MyExecutable/
    │ └── main.swift # 可执行文件入口
    ├── Tests/ # (可选)
    └── Package.swift # 清单文件

生成的 Package.swift 文件类似这样(库类型):

```swift
// swift-tools-version:5.7 // 指定了构建该包所需的 Swift 工具链版本
import PackageDescription

let package = Package(
name: "MyLibrary", // 包名
products: [
// 定义库产品,供外部使用
.library(
name: "MyLibrary",
targets: ["MyLibrary"]), // 该库产品包含 MyLibrary 目标
],
dependencies: [
// 在这里声明外部依赖
// .package(url: / package url /, from: "1.0.0"),
],
targets: [
// 定义常规目标
.target(
name: "MyLibrary",
dependencies: []), // 该目标依赖的其他目标(可以是外部包的目标)
// 定义测试目标
.testTarget(
name: "MyLibraryTests",
dependencies: ["MyLibrary"]), // 测试目标依赖于 MyLibrary 目标
]
)
```

2. 在 Xcode 项目中添加 SPM 依赖

这是 iOS/macOS 应用开发中最常见的场景。

  1. 打开 Xcode 项目: 确保你的项目是用 Xcode 11 或更高版本创建或打开的。
  2. 导航到项目设置: 在项目导航器中选择你的项目文件(蓝色图标)。
  3. 选择 "Package Dependencies" 标签: 在项目编辑器的顶部,找到并点击 "Package Dependencies" 标签页。
  4. 添加包: 点击 "+" 按钮。Xcode 会弹出一个窗口。
  5. 搜索或输入包 URL:
    • 你可以在右上角的搜索框中搜索已知的公开包(如 Alamofire, Kingfisher 等)。Xcode 会搜索 Swift Package Index 或 GitHub。
    • 或者,直接粘贴你想添加的包的 Git 仓库 URL (例如 https://github.com/Alamofire/Alamofire.git)。
  6. 选择版本规则: Xcode 会自动检测包的可用版本。你需要为这个依赖选择一个版本规则:
    • Up to Next Major Version (推荐): 例如 5.0.0 ..< 6.0.0。允许获取所有 5.x.y 的更新,但不会升级到 6.0.0,以避免破坏性更改。这是最常用的规则,遵循语义化版本控制 (Semantic Versioning)。
    • Up to Next Minor Version: 例如 5.4.0 ..< 5.5.0。只允许获取补丁更新 (5.4.x)。
    • Exact Version: 指定一个确切的版本号,例如 5.4.3。不会自动更新。
    • Branch: 直接跟踪某个分支(如 maindevelop)。适用于开发阶段或测试最新功能,但不推荐用于生产环境,因为分支代码不稳定。
    • Commit: 跟踪某个特定的 Git Commit 哈希。
  7. 添加包: 点击 "Add Package"。Xcode 会开始解析依赖关系(这可能需要一些时间,因为它需要检查所有传递依赖)。
  8. 选择要添加到 Target 的产品: 解析完成后,Xcode 会显示该包提供的所有产品(通常是库)。勾选你需要在项目中使用的库,并选择要将这些库链接到的目标(你的 App Target 或某个 Framework Target)。
  9. 完成: 点击 "Add Package"。

现在,你就可以在你的代码中 import 这个库并使用它了。Xcode 会自动处理库的下载、编译和链接。

你可以在 "Package Dependencies" 标签页查看和管理所有已添加的包,包括更新版本 (File > Packages > Update to Latest Package Versions) 或解决版本冲突 (File > Packages > Resolve Package Versions)。

3. 在 Swift 包中添加依赖

如果你正在开发一个 Swift 包(库或命令行工具),并且需要依赖其他 SPM 包,你需要在 Package.swift 文件中声明这些依赖。

修改 Package.swift 文件,在 dependencies 数组中添加条目:

```swift
// swift-tools-version:5.7
import PackageDescription

let package = Package(
name: "MyLibrary",
platforms: [ // 可选:指定支持的最低平台版本
.macOS(.v10_15),
.iOS(.v13),
],
products: [
.library(
name: "MyLibrary",
targets: ["MyLibrary"]),
],
dependencies: [
// 示例:添加 Alamofire 依赖,要求版本 5.6.0 或更高,但低于 6.0.0
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.0")),
// 示例:添加另一个本地包作为依赖(假设在同一目录下)
// .package(path: "../MyOtherLocalPackage"),
// 示例:添加依赖并指定精确版本
// .package(url: "https://github.com/some/other.git", .exact("1.2.3")),
// 示例:添加依赖并跟踪特定分支
// .package(url: "https://github.com/some/dev-package.git", .branch("develop")),
],
targets: [
.target(
name: "MyLibrary",
dependencies: [
// 在目标中引用依赖包提供的产品(库)
// 需要使用 Product 名称,而不是 Package 名称
.product(name: "Alamofire", package: "Alamofire"),
// 如果依赖是本地包,也类似引用
// .product(name: "MyOtherLocalPackageLibrary", package: "MyOtherLocalPackage"),
],
resources: [ // 可选:如果目标包含资源文件
.process("Resources") // 处理 Resources 目录下的资源
]
),
.testTarget(
name: "MyLibraryTests",
dependencies: ["MyLibrary"]),
]
)
```

重要说明:

  • dependencies 数组声明了包需要哪些外部包。
  • targets 中的 dependencies 数组指定了 特定目标 需要链接哪些库。这些库可以来自同一个包的其他目标,也可以来自 dependencies 中声明的外部包的产品。
  • 引用外部包产品时,使用 .product(name: "ProductName", package: "PackageName")ProductName 是该依赖包在其 Package.swift 中定义的产品名,PackageName 是该依赖包在其 Package.swift 中定义的包名(通常与 Git 仓库名相关,但不完全等同,以其 Package.swift 文件为准)。

修改完 Package.swift 后,SPM 会在下次构建或解析时(例如在终端运行 swift build 或在 Xcode 中操作)自动获取并集成新的依赖。

四、Package.swift 清单文件详解

Package.swift 是理解和配置 SPM 的关键。让我们更深入地了解其主要组成部分:

  • // swift-tools-version: 注释: 必须是文件的第一行,指定了编写和解析该清单文件所需的最低 Swift 工具链版本。这确保了向后兼容性。
  • import PackageDescription: 导入定义了 Package, Target, Product 等类型的模块。
  • Package 初始化器:
    • name: 包的名称。SPM 会根据这个名称推断库产品的默认名称和目标的默认名称(如果未显式指定)。
    • platforms: (可选) 指定包支持的最低操作系统版本。这有助于确保包不会在不兼容的系统上被使用。
    • products: 定义包向外部提供的产出物(库、可执行文件)。如果你的包只是应用程序内部使用的模块,可以省略此项。
    • dependencies: 声明该包所依赖的其他 SPM 包。
    • targets: 定义组成包的各个模块(源代码和资源)。
    • swiftLanguageVersions: (可选) 指定包支持的 Swift 语言版本。
    • pkgConfig: (可选) 用于链接系统库(通过 pkg-config)。
    • providers: (可选) 用于在 Linux 上安装系统库依赖(如 apt, yum)。

依赖版本规则详解:

  • .from(Version): 例如 .from("1.2.3")。相当于 >= 1.2.3< 2.0.0 (即 1.2.3 ..< 2.0.0)。这是 .upToNextMajor 的简写形式。
  • .upToNextMajor(from: Version): 例如 .upToNextMajor(from: "1.2.3")。同上,允许非破坏性的次版本和补丁更新。推荐使用。
  • .upToNextMinor(from: Version): 例如 .upToNextMinor(from: "1.2.3")。相当于 >= 1.2.3< 1.3.0 (即 1.2.3 ..< 1.3.0)。只允许补丁更新。
  • .exact(Version): 例如 .exact("1.2.3")。只使用指定的精确版本。
  • .branch(String): 例如 .branch("main")。跟踪指定分支的最新提交。不稳定,慎用。
  • .revision(String): 例如 .revision("abcdef123...")。跟踪指定的 Git Commit 哈希。
  • 范围操作符:也可以使用范围,如 "1.2.3" ..< "1.3.0""1.2.3" ... "1.5.0"

目标配置详解:

  • .target(name:dependencies:path:exclude:sources:resources:publicHeadersPath:cSettings:cxxSettings:swiftSettings:linkerSettings):
    • name: 目标名称。
    • dependencies: 该目标依赖的其他目标或产品。可以是:
      • .target(name: "OtherTargetInSamePackage")
      • .product(name: "ProductName", package: "PackageName")
      • .byName(name: "ImplicitTargetOrProduct") (根据名称自动推断)
    • path: (可选) 自定义源代码路径,默认为 Sources/<TargetName>
    • exclude: (可选) 从源文件列表中排除特定文件或目录。
    • sources: (可选) 显式指定源文件列表(不常用)。
    • resources: (可选) 处理资源文件。[.process("Resources"), .copy("Assets")].process 会针对平台优化(如编译 xcassets),.copy 则直接复制。
    • publicHeadersPath: (可选) C/Objective-C 混编时,公开头文件的路径。
    • cSettings, cxxSettings, swiftSettings: (可选) 特定于语言的编译设置(如宏定义、头文件搜索路径、编译器标志)。
    • linkerSettings: (可选) 链接器设置(如链接库、链接器标志)。
  • .testTarget(...): 与 .target 类似,但用于测试,默认路径为 Tests/<TargetName>。通常依赖于被测试的 .target
  • .binaryTarget(name:path:).binaryTarget(name:url:checksum:): 引用预编译的 .xcframework。可以通过本地路径或远程 URL 添加。URL 方式需要提供校验和以确保安全性。

五、SPM 与 Xcode 的深度集成

Xcode 为 SPM 提供了强大的图形化界面支持和后台管理能力:

  • 包依赖管理: 如前所述,可以通过 Xcode UI 添加、更新、移除 SPM 依赖。
  • 自动解析与构建: Xcode 会自动处理依赖解析、下载和构建过程。编译后的产物通常存储在项目的 DerivedData 目录下。
  • Package.resolved 文件: Xcode (或 SPM 命令行) 在成功解析依赖后会生成或更新此文件。它精确记录了当前项目中所有依赖(包括传递依赖)所使用的具体版本(精确到 Commit 哈希)。建议将 Package.resolved 文件提交到版本控制系统 (Git),以确保团队成员和 CI/CD 环境使用完全相同的依赖版本,实现可复现的构建。
  • 源代码查看与调试: 添加的 SPM 依赖的源代码会出现在 Xcode 的项目导航器中(通常在一个 "Package Dependencies" 分组下),你可以像浏览自己的代码一样查看、跳转定义,甚至设置断点进行调试。
  • 本地包开发: 你可以将本地开发的 SPM 包直接拖拽到 Xcode 项目的根目录下(或项目导航器中)。Xcode 会识别它并将其作为本地包依赖进行管理。这对于同时开发 App 和其依赖的本地库非常方便,修改库代码后可以立即在 App 中看到效果,无需发布或提交。

六、SPM 的优势与适用场景

优势:

  1. 官方标准: Apple 生态系统的未来方向,获得持续改进和支持。
  2. 无缝集成: 与 Xcode 配合极佳,使用体验流畅。
  3. 简洁配置: Package.swift 使用 Swift 编写,类型安全,易于理解和维护。
  4. 去中心化: 发布和获取包更直接,不易受单点故障影响。
  5. 构建速度: 对于某些项目配置,SPM 可能比 CocoaPods(特别是使用 use_frameworks! 时)有更快的增量构建速度,因为它通常倾向于构建静态库。
  6. 跨平台: 支持 macOS, iOS, watchOS, tvOS 以及 Linux。

适用场景:

  • 新项目: 对于所有新开始的 iOS/macOS 项目,优先考虑使用 SPM 管理依赖。
  • 库开发: 创建 Swift 库(无论是开源还是内部使用),SPM 是标准的打包和分发方式。
  • 命令行工具: 使用 Swift 开发跨平台命令行工具。
  • 逐步迁移: 对于使用 CocoaPods 或 Carthage 的现有项目,可以考虑逐步将部分依赖迁移到 SPM。许多流行的库现在都支持 SPM。

七、与 CocoaPods 和 Carthage 的比较

  • CocoaPods:
    • 优点: 历史悠久,社区庞大,支持 Objective-C 和 Swift,功能丰富(如资源处理、Podfile 配置灵活)。
    • 缺点: 中心化的 Specs repo 可能成为瓶颈,需要运行 pod install/update,对 Xcode 项目文件侵入性较强(创建 Workspace,修改 Build Settings),可能影响构建性能。
  • Carthage:
    • 优点: 去中心化,对项目侵入性小(只负责构建 Framework,由开发者手动集成),更接近 Xcode 原生构建流程。
    • 缺点: 需要手动将构建好的 Framework 添加到项目中并处理链接,配置相对 SPM 和 CocoaPods 复杂一些,社区活跃度和官方支持不如前两者。
  • SPM:
    • 优点: 官方、集成度高、配置简洁、去中心化、跨平台。
    • 缺点: 对于非常复杂的依赖配置或特殊的构建需求(如复杂的预/后处理脚本),可能不如 CocoaPods 灵活。对 Objective-C 项目和混合项目的支持虽然在改进,但初期不如 CocoaPods 成熟。二进制依赖的支持是后来才完善的。

目前,SPM 已足够成熟,能够满足绝大多数 iOS/macOS 项目的依赖管理需求。

八、高级话题与最佳实践

  • 资源文件: 使用 .target 中的 resources 参数来包含和处理资源文件(如图片、JSON、xcassets、Storyboard/XIB 等)。使用 Bundle.module 在代码中访问包内的资源。
  • 二进制依赖 (.binaryTarget): 用于分发闭源库或包含大型预编译资源的库。需要提供 .xcframework 格式。
  • 插件 (Plugins): SPM 支持构建插件(Build Tool Plugins)和命令插件(Command Plugins),允许在构建过程中或通过命令行执行自定义脚本和工具,例如代码生成、代码检查等。这是一个较新的高级功能。
  • 本地包: 如前所述,利用本地包进行模块化开发和调试非常方便。
  • 语义化版本控制 (SemVer): 严格遵循 SemVer (主版本号.次版本号.修订号) 对 SPM 的版本解析至关重要。发布包时务必打上正确的 Git Tag。
  • 保持更新: 定期使用 File > Packages > Update to Latest Package Versions 更新依赖,以获取 bug 修复和新功能,同时注意检查是否有破坏性更新(主版本号变更)。
  • Package.resolved 的管理: 务必将其纳入版本控制,确保构建一致性。

九、总结

Swift Package Manager 已经从一个主要面向 Swift 服务端和工具开发的工具,演变成了 Apple 生态系统中强大且主流的依赖管理解决方案。它与 Xcode 的深度集成、简洁的配置方式以及官方支持,使其成为 iOS 和 macOS 开发者的重要技能。通过理解其核心概念(包、目标、产品、依赖、清单文件),掌握在 Xcode 中添加依赖或创建 SPM 包的基本操作,并了解 Package.swift 的详细配置,你将能够有效地利用 SPM 来管理项目依赖,提高开发效率和代码质量。随着 Swift 和 Apple 生态的不断发展,SPM 的重要性只会与日俱增,现在正是全面拥抱它的最佳时机。


THE END