全面了解 Swift Package Manager (SPM) – wiki基地


全面了解 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 应用开发中最常用的方式。

  1. 打开项目: 在 Xcode 中打开你的 .xcodeproj.xcworkspace 文件。
  2. 导航到项目设置: 在 Project Navigator (左侧导航栏) 中选中你的项目根节点。
  3. 选择 Swift Packages 标签页: 在主编辑区域,选择 “Swift Packages” 标签页。
  4. 点击添加按钮: 点击左下角的 + 按钮。
  5. 输入仓库 URL: 在弹出的搜索框中输入你想要添加的 Swift 包的 Git 仓库 URL(例如 https://github.com/Alamofire/Alamofire.git)。Xcode 会连接到 Git 仓库并展示可用的版本规则。
  6. 选择版本规则: 根据你的需求选择依赖的版本规则。常见的选项包括:
    • Up to Next Major Version (推荐用于应用程序): 依赖于指定的起始版本(例如 5.4.0)及后续所有次要版本和补丁版本,直到下一个主版本发布。这样可以在不引入潜在的破坏性变更的情况下获得 bug 修复和新特性。
    • Up to Next Minor Version: 依赖于指定的起始版本及后续所有补丁版本,直到下一个次要版本发布。适用于对依赖版本要求更严格的场景,如作为库发布的包。
    • Exact Version: 精确锁定到某个特定的版本。通常用于需要极高版本稳定性的场景,或作为临时解决方案。
    • Branch: 依赖于仓库的某个特定分支。通常用于开发或测试尚不稳定版本的依赖。
    • Commit: 依赖于仓库的某个特定的提交哈希。用于锁定到非常具体的代码状态,但不推荐在生产环境使用,因为缺乏可读性且难以管理。
  7. 添加到目标: 选择你希望哪些项目目标(Targets,如你的 App Target 或某个 Framework Target)依赖这个包。SPM 会将包的产品(通常是库产品)链接到这些目标中,使你可以在这些目标的源代码中导入并使用它。
  8. 完成: 点击 “Add Package” 按钮。Xcode 会自动下载、解析依赖关系并构建包。完成后,你会在 Project Navigator 的 “Swift Package Dependencies” 部分看到添加的包。

现在你可以在你选择的目标的 Swift 代码中 import YourPackageModule 来使用这个包提供的功能了。

3.2 使用命令行添加依赖 (适用于纯 Swift 包或 CLI 工具)

对于不使用 Xcode 项目的纯 Swift 包、命令行工具或跨平台项目,可以使用 Swift CLI 命令来管理依赖。

  1. 打开终端: 导航到你的包或项目的根目录(包含 Package.swift 文件)。
  2. 编辑 Package.swift 使用文本编辑器打开 Package.swift 文件。
  3. 添加依赖项:dependencies 数组中添加新的 .package 条目,指定仓库 URL 和版本规则。例如:
    swift
    dependencies: [
    .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
    // 添加其他依赖...
    ],
  4. 将依赖的产品添加到目标: 在需要使用这个依赖的目标的 dependencies 数组中,添加对该依赖包中某个产品的引用。例如:
    swift
    targets: [
    .executableTarget(
    name: "MyCLI",
    dependencies: [
    .product(name: "ArgumentParser", package: "swift-argument-parser") // 引用 swift-argument-parser 包的 ArgumentParser 产品
    ]),
    // 添加其他目标...
    ]
  5. 解析并获取依赖: 在终端中运行以下命令:
    bash
    swift package resolve

    这个命令会根据 Package.swift 文件解析所有依赖,下载它们(如果尚未下载),并更新 Package.resolved 文件。
  6. 构建项目: 运行 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 无法找到一个兼容的版本,它会报错并告诉你冲突所在。

解决依赖冲突通常需要:

  1. 查看 SPM 报告的冲突信息,确定是哪个共享依赖引起的问题。
  2. 检查引起冲突的直接依赖(你的项目直接依赖的包)是否可以使用一个允许更高版本共享依赖的版本。
  3. 有时候,可能需要手动调整你的某个直接依赖的版本规则,使其允许使用一个与另一个依赖兼容的共享库版本。
  4. 如果冲突无法通过调整版本规则解决,可能需要等待某个依赖库发布兼容新版本的更新,或者考虑替换其中一个冲突的依赖库。

五、 创建自己的 Swift Package

SPM 不仅是消费依赖的工具,更是创建和分发 Swift 代码模块的标准方式。

5.1 创建一个新包

使用 Swift CLI 命令可以快速创建一个新的 Swift 包:

“`bash

创建一个库包

swift package init –type library MyNewLibrary

创建一个可执行包 (CLI 工具)

swift package init –type executable MyNewCLI
“`

这会在当前目录下创建一个名为 MyNewLibraryMyNewCLI 的文件夹,其中包含基本的目录结构和一个初始的 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 只会编译 publicinternal 级别的符号。如果你希望你的代码可以在其他模块中被 import 使用,确保使用 public 访问控制修饰符。

5.5 添加资源和本地化

SPM 支持在包中包含资源文件(图片、音频、JSON 等)和本地化字符串。

  1. 将资源文件放入目标目录下的 Resources 子目录:
    Sources/MyAwesomeLibrary/
    ├── MyAwesomeLibrary.swift
    └── Resources/
    ├── logo.png
    └── strings.txt
  2. Package.swift 中配置目标: 在对应的 .target 定义中添加 resources 参数。
    swift
    .target(
    name: "MyAwesomeLibrary",
    dependencies: [],
    resources: [.process("Resources")]), // 指定处理 Resources 目录下的所有文件

    .process 表示 SPM 会根据平台和文件类型智能地处理资源(例如,在 macOS 上直接复制,在 iOS 上可能打包进 Asset Catalogs)。.copy 则只是简单地复制文件。
  3. 在代码中访问资源: 资源会被 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 包时,你可能希望在一个实际的应用项目中测试这个包。

  1. 创建本地包: 按照前述步骤创建一个本地 Swift 包。
  2. 将本地包添加到 Xcode 项目:
    • 在 Xcode 的 Project Navigator 中,右键点击你的项目或工作空间。
    • 选择 “Add Packages…”。
    • 在搜索框下方,选择 “Add Local…” 按钮。
    • 导航到你的本地包所在的目录并选择它。
    • Xcode 会像处理远程依赖一样处理这个本地包,并让你选择将其添加到哪些目标。
  3. 开发和测试: 现在你可以在 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 resolveswift 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 吧!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部