WWDC 2019:在 Xcode 中使用 Swift Package

本文同步发表在小专栏 WWDC19 内参
WWDC 2019 Session 408: Adopting Swift Packages in Xcode

目前,在 macOS/iOS 开发中,我们通常使用 CocoaPodsCarthage 等非官方工具来管理项目工程中对第三方开源库的依赖。

Swift Package Manager(Swift 包管理器,一般简称 SwiftPM 或者 SPM)是苹果官方提供的一个用于管理源代码分发的工具,旨在使分享代码和复用其他人的代码变得更加容易。该工具可以直接帮助我们编译和链接 Swift packages(包),管理依赖关系、版本控制,以及支持灵活的代码分发和团队协作。

关于 Swift Package Manager 的详细介绍,可以参考去年的 WWDC 2018 Session 411: Getting to Know Swift Package Manager,以及我总结的这篇文章:

SwiftPM 一开始仅支持 macOS 和 Linux 平台上的 Swift 开发,且只能通过命令行的方式来使用。在最新的 Xcode 11 中集成了 libSwiftPM,并提供了图形化操作界面,使 Swift Package 支持 iOS/watchOS/tvOS 等平台。

下文我们将介绍如何在 Xcode 11 中使用 Swift Package 引入第三方开源库或私有代码库。

如何添加 Package

我们先通过 Xcode 11 新建一个 Swift iOS 工程,假设名为 MyTestProject,然后可以通过如下两种方式添加 Swift Package 依赖:

  • Xcode Menu -> File -> Swift Packages -> Add Package Dependency…

  • 在 Xcode 工程中选中当前 Project 名称 -> 选择 Swift Packages -> 点击 + 图标添加

在弹出的窗口中,我们可以输入要依赖的 Package 的 git 仓库地址,例如,我们这里要在工程中添加 Yams 这个 Swift YAML 文件解析的开源库,则可以在输入框中填写其在 GitHub 上的 git url,如下图所示:

此外,我们可以在 Xcode -> Preferences… -> Accounts 中添加并登录自己的 GitHub/GitLab 账号或者公司内部私有 Git 服务器账号,然后就可以在 Choose Package Repository 窗口中直接选择你自己的或者已关注的 Swift Package,如下图:

选择好要依赖的 Package 后,点击 Next 按钮进行版本号设置。我们可以指定 Package 的版本号范围,规则如下,与 CocoaPods 类似:

  • Up to Next Major: 当前指定的版本号到下一个大版本号之间的最新版本,例如 2.0.0 ~ 3.0.0(不包含 3.0.0)
  • Up to Next Minor: 当前指定的版本号到下一个次版本号之间的最新版本,例如 2.0.0 ~ 2.1.0(不包含 2.1.0)
  • Range: 指定的两个版本号之间的最新版本,例如 2.1.0 ~ 2.7.2(不包含 2.7.2)
  • Exact: 指定使用某一具体的版本号

同时,我们也可以指定要依赖当前 Package git 仓库的某一个分支或者某一次 commit。

最后,勾选当前 Package 要添加到工程中的哪些 Targets,即可。

添加好 Package 之后,我们就可以在 Xcode 工程中查看到相关信息,如下图:

接下来我们就可以在代码中 import Yams 然后调用它的相关 API 了。

Package 概览

下面我们介绍一下 Swift Package 的内部结构。

一个 Package(包)由 Swift 源码文件和一个清单文件组成。这个清单文件被命名为 Package.swift,它使用 PackageDescription 模块来定义包的名称、内容以及依赖关系。

Yams 为例,它包含的内容如下:

  • Package.swift: 包的清单文件,用于描述包的名称、内容、依赖关系、支持的 Swift 版本号;
  • Sources: 源码文件夹,通常包括 C/C++ 代码和 Swift 代码等;
  • Tests: 单元测试代码

Package.swift 文件的大致内容如下:

另外,如果一个 Package 依赖了另一个 Package,也需要在 Package.swift 文件中进行声明。例如,有一个 Package 叫 “DesignTheme”,它依赖了 “DesignFont”,则需要在 “DesignTheme” 的 Package.swift 文件中添加如下依赖代码:

1
2
3
dependencies: [
.package(url: "http://github.com/WWDC19/DesignFont.git"),
]

此时,当在 Xcode 工程的 Swift Packages 中添加了 “DesignTheme”,同时就会自动下载依赖 “DesignFont”,不需要我们再手动添加了。

因此,对于一个 Xcode 工程,当其依赖了一些 Swift Packages 后,在编译链接时,SwiftPM 会自动编译每个 Package 并链接到主可执行文件中。

PS: 对于 SwiftPM 的一些基本概念,例如:Modules、Packages、Products、Dependencies、Targets 等,在 Swift.org 官网已经有非常详细的描述和定义,另外,也可以参见我之前写的这篇文章,这里不再赘述。

Package 依赖详解

本节我们详细介绍一下 Swift Package 之间互相依赖的一些细节。

如前面所述,假如我们在 Demo 工程(名为 “Lunch”)中,除了引入 “Yams” Package 外,又引入了一个 “DesignTheme” Package,如下图:

此时,如果 “DesignTheme” Package 又同时依赖 “DesignFont” 和 “DesignColor” 两个库,我们就会在 Xcode 工程的 Package 依赖列表中看到自动引入了这两个依赖的 Package:

那 Xcode 是如何找到他们的依赖关系和版本号信息呢?

正如前面所说的,其实在 “DesignTheme” 的 Package.swift 文件中有详细描述这些依赖关系,然后 Xcode 会根据该文件的信息来选择下载对应 Package 的最佳版本号:

另外一点需要注意的是,我们在上述 Demo 工程的 Swift Packages 中,仅添加了对 “Yams” 和 “DesignTheme” 两个 Packages 的依赖,所以我们可以在工程代码中直接 import Yamsimport DesignTheme 然后调用它们相关的 API:

而另外两个 Packages “DesignFont” 和 “DesignColor” 是由 “DesignTheme” 的依赖隐式引入的,所以我们不能在代码中直接 import 使用它们。

假如 Demo 工程中需要用到 “DesignFont” 相关的 API,则需要在 Xcode 工程的 Swift Packages 列表中显式地添加依赖才行。

如何更新 Package

当我们的 Xcode 工程添加了一些 Swift Package 后,如果其中的一些 Package 有了新版本,例如,上述 “DesignFont” 我们一开始依赖的是 1.2.0,后面其修复了 bug 并发布了新版本 1.2.1,我们该如何更新呢?

其实非常方便,我们可以在 Xcode Menu 中点击 File -> Swift Packages,然后选择 “Update to Latest Package Versions” 即可,如下图所示,此时当前工程中所有的 Packages 都会自动更新到指定版本范围内的最新版本

那么,Xcode 更新一个 Package 时,除了下载更新最新的代码外,还做了哪些事情呢?

其实,Xcode 维护了一个 Package.resolved 文件用于记录当前工程已添加的各个 Packages 的详细信息,包含名称、URL、分支和具体的版本号等信息,位于 .xcodeproj 里,路径为:

Package.resolved 是一个 JSON 文件,内容大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"object": {
"pins": [
{
"package": "Yams",
"repositoryURL": "git@github.com:jpsim/Yams.git",
"state": {
"branch": null,
"revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f",
"version": "2.0.0"
}
},
// other packages
]
},
"version": 1
}

当某一个 Package 更新时,上述文件中该 Package 对应的 revisionversion 就会被修改为最新信息,如下图所示,”DesignFont” 从 1.2.0 更新到 1.2.1:

所以,与 CocoaPods 的 Podfile.lock 和 Carthage 的 Cartfile.resolved 文件作用类似,当一个工程涉及到多人协作开发时,我们就应该在 git 仓库中提交 Package.resolved 文件,保证团队中每个人用的 Packages 都是同一个版本,避免带来不必要的问题。

解决 Package 版本冲突

仍然以第三节举的 Demo 工程为例,其中,我们依赖的 “DesignTheme” 的版本号设置为 1.0.0 - Next Major,而 “DesignTheme” 的 Package.swift 文件中声明的两个依赖 “DesignFont” 和 “DesignColor” 对应的版本号也为 1.0.0 - Next Major,此时,这 3 个 Packages 在 Xcode 中最终被引入的版本号如下:

由于 “DesignTheme” 已经隐式引入 1.2.0 版本的 “DesignFont” 了,这个时候,如果我们要在 Demo 工程的 Swift Packages 中直接显式地引入 “DesignFont” 的 2.0.0 以上版本,就会报错,存在版本冲突。

这是因为,在一个 Xcode 工程里(workspace),对于同一个 Package,只能引入一个版本号。

那么如何解决这个问题呢?

只能是更新 “DesignTheme” 到 2.0.0,并在其 Package.swift 文件中声明依赖的 “DesignFont” 的版本号也从 2.0.0 起。然后,我们再在 Xcode 工程的 Swift Packages 列表中显式地添加 2.0.0 起的 “DesignFont”。

如何创建 Packages

本文主要讲解如何在最新的 Xcode 11 中使用 Swift Package,如果你想学习如何创建一个 Package 用于在 GitHub 上开源或者团队内部使用,可以阅读以下两个相关 Session:

结语

在 GitHub 上,绝大部分 Swift 开源库都已经支持通过 SwiftPM 来接入,所以如果你的 Xcode 工程之前有使用一些第三方的 Swift 库,不妨改成用 Xcode 11 自带的方式来导入试试吧。