大部分语言都有官方的代码分配解决方案,幸好苹果也在开发能替代 CocoapodsCarthage 的管理工具。Swift Package Manager(Swift 包管理器,下面我们简称 SPM )就是一个用来管理 Swift 代码的分配的官方工具,它为 Swift 编译系统集成了自动进行下载、编译和连接依赖的过程。

目前,SPM 还处于早期阶段,现在仅支持 OS X 和 Linux 系统,尚不支持 iOS, watchOS 以及 tvOS 平台,但未来很大希望会支持上述平台。

本文使用苹果官方的例子来讲解 SPM 的使用,希望这篇文章能够帮到有意进行尝试的人! :)

概念概述

这个章节我们先来了解一下关于 SPM 功能的一些基本概念。

模块

在 Swift 中我们使用模块来管理代码,每个模块指定一个命名空间并强制指定模块外哪些部分的代码是可以被访问控制的。

一个程序可以将它所有代码聚合在一个模块中,也可以将它作为依赖关系导入到其他模块。除了少量系统提供的模块,像 OS X 中的 Darwin 或者 Linux 中的 Glibc 等的大多数依赖需要代码被下载或者内置才能被使用。

当你将编写的解决特定问题的代码独立成一个模块时,这段代码可以在其他情况下被重新利用。例如,一个模块提供了发起网络请求的功能,在一个照片分享的 app 或者 一个天气的 app 里它都是可以使用的。使用模块可以让你的代码建立在其他开发者的代码之上,而不是你自己去重复实现相同的功能。

一个包由 Swift 源文件和一个清单文件组成。这个清单文件称为 Package.swift,定义包名或者它的内容使用 PackageDescription 模块。

一个包有一个或者多个目标,每个目标指定一个产品并且可能声明一个或者多个依赖。

产品

一个目标可能构建一个库或者一个可执行文件作为其产品。库是包含可以被其他 Swift 代码导入的模块。可执行文件是一段可以被操作系统运行的程序。

依赖

目标依赖是指包中代码必须添加的模块。依赖由包资源的绝对或者相对 URL 和一些可以被使用的包的版本要求所组成。包管理器的作用是通过自动为工程下载和编译所有依赖的过程中,减少协调的成本。这是一个递归的过程:依赖能有自己的依赖,其中每一个也可以具有依赖,形成了一个依赖相关图。包管理器下载和编译所需要满足整个依赖相关图的一切。

下一个章节可能需要你了解一些 Swift 的基本知识,如果你是 Swift 的新手,可以先进这个传送门学习一下 Swift 的基本知识: Swift 入门教程

开源 Swift 入门

接下来,我们还需要了解一下开源 Swift 的一些入门知识。

  • 下载和安装 Swift
  • 使用 REPL
  • 使用编译系统
  • 使用 LLDB 调试器

由于本文重点不在这里,所以我们就简单介绍下如何下载和安装 Swift 以及如何使用编译系统, 关于 使用 REPL 和 LLDB 调试器的内容具体可以参阅官方文档 使用 REPL使用 LLDB 调试器

下载和安装 Swift

刚开始使用 Swift 需要下载并安装编译器和其他必备组件。进入到 https://swift.org/download/#releases 按目标平台的说明进行。

下载的时候需要注意下,由于 SPM 还不是特别的成熟,我们在这个教程中将不使用 Releases 的 Swift,我们需要下载开发版的 Snapshots。如下图所示,我们使用日期为 April 12, 2016 的 Snapshots,另外千万不要点击下载 Debugging Symbols 或者 Signature 的安装包,因为那样会缺失一些内置的依赖库!!

image

下载完成后点击按步骤安装就可以了!

OS X

在 OS X 上下载工具链默认的地址是 /Library/Developer/Toolchains。接着我们输入以下命令导出编译路径

1
$ export PATH=/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin:"${PATH}"

Linux

首先需要安装 clang :

1
$ sudo apt-get install clang

如果你在 Linux 上安装的 Swift 工具链在系统根目录以外的目录,你需要使用你安装 Swift 的实际路径来运行下面的命令:

1
$ export PATH=/path/to/Swift/usr/bin:"${PATH}"

导出路径之后,你可以通过输入 swift 命令并传入 --version 标志来校验你是否运行了 Swift 的预期版本

1
2
$ swift --version
Apple Swift version 3.0-dev (LLVM ..., Clang ..., Swift ...)

在版本号的后缀-dev用来表明它是一个开发的编译,而不是一个发布的版本。

使用编译系统

Swift 编译系统为编译库、可执行文件和在不同工程之间共享代码提供了基本的约定。

创建包

创建一个新的 Swift 包,首先创建并进入到一个新的目录命名为 Hello

1
2
$ mkdir Hello
$ cd Hello

每个包在其根目录下都必须拥有一个命名为 Package.swift 清单文件。如果清单文件为空,那包管理器将会使用常规默认的方式来编译包。创建一个空的清空文件使用命令:

1
$ touch Package.swift

当使用默认方式时,包管理器预计将包含在 Sources/ 子目录下的所有源代码。创建方式:

1
$ mkdir Sources

编译可执行文件

默认方式下,目录中包含一个文件称为 main.swift 将会将文件编译成与包名称相同的二进制可执行文件。

在这个例子中,包将生成一个可以输出 Hello, world! 的可执行文件命名为 Hello

Sources/ 目录下创建一个命名为 main.swift 的文件,并使用你喜欢的任意一种编辑器输入如下代码:

1
print("Hello, world!")

返回到 Hello 目录中,通过运行 swift build 命令来编译包:

1
$ swift build

当命令完成之后,编译产品将会出现在 .build 目录中。通过如下命令运行 Hello 程序:

1
2
$ .build/debug/Hello
Hello, world!

下一步,让我们在新的资源文件里定义一个新的方法 sayHello(_:) 然后直接用 print(_:) 替换执行调用的内容。

多个源文件协作

Sources/ 目录下创建一个新文件命名为 Greeter.swift 然后输入如下代码:

1
2
3
func sayHello(name: String) {
print("Hello, \(name)!")
}

sayHello(_:) 方法带一个单一的字符串参数,然后在前面打印一个 “Hello”,后面跟着函数参数单词 “World”。

现在打开 main.swift, 然后替换原来的内容为下面代码:

1
2
3
4
5
6
if Process.arguments.count != 2 {
print("Usage: hello NAME")
} else {
let name = Process.arguments[1]
sayHello(name)
}

跟之前的硬编码不同,main.swift 现在从命令行参数中读取。替代之前直接调用 print(_:)main.swift 现在调用 sayHello(_:) 方法。因为这个方法是 Hello 模块的一部分,所以不需要使用到 import 语句。

运行 swift build 并尝试 Hello 的新版本:

1
2
$ swift build
$ .build/debug/Hello 'whoami'

目前为止,你已经能够运用开源 Swift 来运行一些你想要的程序了。接下来我们就可以进入正题开始入手 SPM 。

快速入门实例

开源 Swift 入门 章节中,我们简单地学会了编译一个 “Hello world!” 程序。

为了了解 SPM 究竟能够做什么,我们来看一下下面这个由4个独立的包组成的例子:

  • O2PlayingCard - 定义了 O2PlayingCardO2SuitO2Rank, 3个类型。
  • O2FisherYates - 定义了 shuffle()shuffleInPlace() 方法实现的扩展。
  • O2DeckOfPlayingCards - 定义了一个 O2Deck 类型对 O2PlayingCard 值得数据进行洗牌和抽牌。
  • O2Dealer - 定义了一个用来创建 O2DeckOfPlayingCards 进行洗牌和抽出前10个卡片的可执行文件。

你可以从 O2Dealer from GitHub 编译并运行完整例子,然后运行如下命令:

1
2
3
$ cd O2Dealer
$ swift build
$ .build/debug/O2Dealer

创建一个库包

我们将从创建一个代表一副标准的52张扑克牌的模块开始。O2PlayingCard 模块定义了 由 O2Suit 枚举值(Clubs, Diamonds, Hearts, spades)和 O2Rank 枚举值(Ace, Two, Three, …, Jack, Queen, King)组成的 O2PlayingCard 类。各个类的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum O2Rank : Int {
case Ace = 1
case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King
}
public enum O2Suit: String {
case Spades, Hearts, Diamonds, Clubs
}
public struct O2PlayingCard {
let rank: O2Rank
let suit: O2Suit
}

一般来说, 一个包包括任何位于 Sources/ 的源文件。

1
2
3
4
5
6
O2PlayingCard
├── Sources
│ ├── O2PlayingCard.swift
│ ├── O2Rank.swift
│ └── O2Suit.swift
└── Package.swift

由于 O2PlayingCard 模块并不会生成可执行文件,这里应该称为。 库表示被编译成一个可以被其他包导入的模块的包。默认情况下,库模块公开所有位于 Sources/ 目录下源代码中声明的公共类型和方法。

运行 swift build 开始启动 Swift 编译的过程。如果一切进行顺利,将会在 .build/debug 目录下生成 O2PlayingCard.build 目录。

接下来我们在 Package.swift 文件中定义包名,代码如下:

1
2
3
4
5
import PackageDescription
let package = Package(
name: "O2PlayingCard"
)

然后我们只要将 O2PlayingCard 提交到 Github 上,并且给他发布一个 Release 版本即可完成该库包。这里可以自己手动添加一个 .gitignore 文件,忽略掉 /.build ,因为我们的包是不需要包括生成的编译结果的内容的。

完整 O2PlayingCard 代码可以在 https://github.com/marklin2012/O2PlayingCard.git 查看。

使用编译配置语句

下一个即将编译的模块是 O2FisherYates。跟之前 O2PlayingCard 有所不同,该模块没有定义新的类。取而代之的是该模块拓展了一个已经存在的特殊的 CollectionTypeMutableCollectionType 接口协议, 用来添加 shuffle() 方法和对应的 shuffleInPlace() 方法。

shuffleInPlace() 方法的实现使用了 Fisher-Yates 经典洗牌算法来随机交换集合中的元素。由于 Swift 标准库没有提供一个随机数生成方法,该方法需要调用一个从系统模块导入的函数。为了使这个方法能够同时适配 OSX 和 Linux, 代码将要用到编译配置语句。

在 OS X 中,系统模块是 Darwin, 提供的函数是 arc4random_uniform(_:)。在 Linux 中, 系统模块是 Glibc, 提供的函数是 random()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#if os(Linux)
import Glibc
#else
import Darwin.C
#endif
public extension Collection {
func shuffle() -> [Generator.Element] {
var array = Array(self)
array.shuffleInPlace()
return array
}
}
public extension MutableCollection where Index == Int {
mutating func shuffleInPlace() {
guard count > 1 else { return }
v
for i in 0..<count - 1 {
#if os(Linux)
let j = Int(random() % (count - i)) + i
#else
let j = Int(arc4random_uniform(UInt32(count - i))) + i
#endif
guard i != j else { continue }
swap(&self[i], &self[j])
}
}
}

剩下的步骤和前面的类似,编译通过后上传到 Github , 发布 Release 版本。

完整的 O2FisherYates 代码可以在如下地址找到 https://github.com/marklin2012/O2FisherYates.git

导入依赖

O2DeckOfPlayingCards包把前两个包聚合到一起:它定义了一个在 O2PlayingCard 数组中使用 O2FisherYatesshuffle() 方法的 Deck 类型。

为了使用 O2FisherYatesO2PlayingCards 模块, O2DeckOfPlayingCards 包必须在 Package.Swift 清单中将上述模块声明为依赖。

1
2
3
4
5
6
7
8
9
10
11
import PackageDescription
let package = Package(
name: "O2DeckOfPlayingCards",
dependencies: [
.Package(url: "https://github.com/marklin2012/O2PlayingCard.git",
majorVersion: 1),
.Package(url: "https://github.com/marklin2012/O2FisherYates.git",
majorVersion: 1),
]
)

每个依赖都需要制定一个源 URL 和版本号。源 URL 是指允许当前用户解析到对应的 Git 仓库。版本号遵循 语义化版本号 2.0.0 的约定,用来决定检出或者使用哪个 Git 标签版本来建立依赖。对于 FisherYatesPlayingCard 这两个依赖来说, 最新的将要被使用的主版本号为 1 (例如: 1.0.0)。

当你运行 swift build 命令时,包管理器将会下载所有的依赖, 并将他们编译成静态库,再把它们链接到包模块中。这样将会使 O2DeckOfPlayingCards 可以访问依赖 import 语句的模块的公共成员。

你可以看到这些资源被下载到你工程根目录的 Packages 目录下,并且会生成编译产品在你工程根目录的 .build 目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
O2DeckOfPlayingcards
├── .build
│ └── debug
│ ├── O2DeckOfPlayingCards.build
│ ├── O2DeckOfPlayingCards.swiftdoc
│ ├── O2DeckOfPlayingCards.swiftmodule
│ ├── O2FisherYates.build
│ ├── O2FisherYates.swiftdoc
│ ├── O2FisherYates.swiftmodule
│ ├── O2PlayingCard.build
│ ├── O2PlayingCard.swiftdoc
│ └── O2PlayingCard.swiftmodule
└── Packages
└── O2FisherYates-1.0.0
│ ├── Package.swift
│ ├── README.md
│ └── Sources
└── O2Playingcard-1.0.1
├── Package.swift
├── README.md
└── Sources

Packages 目录包含了被复制的包依赖的所有仓库。这样将使你能修改源代码并直接推送这些修改到他们的源,而不需要再对每个包在单独进行复制。剩下的步骤参考前面内容。

完整的 O2DeckOfPlayingCards 包可以在如下地址查看: https://github.com/marklin2012/O2DeckOfPlayingCards.git

解决子依赖

到这一步,你已经可以建立 O2Dealer 模块了。 O2Dealer 模块依赖于 O2DeckOfPlayingCards 包,而该模块又依赖于 O2PlayingCardO2FisherYates 包。然而,由于 SPM 会自动解决子依赖,所以你需要声明 O2DeckOfPlayingCards 包作为依赖即可。

1
2
3
4
5
6
7
8
9
import PackageDescription
let package = Package(
name: "O2Dealer",
dependencies: [
.Package(url: "https://github.com/marklin2012/O2DeckOfPlayingCards.git",
majorVersion: 1),
]
)

对于在代码中引用任意类型, Swift 需要在源文件中导入模块。在 O2Dealer 模块的 main.swift 文件中, O2DeckOfPlayingCards 中的 O2Deck 类型和 O2PlayingCardO2PlayingCard 类型是引用。虽然 O2Deck 类型中的 shuffle() 方法使用在 O2FisherYates 模块内,但是 O2FisherYates 模块并不需要在 main.swift 中被导入。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import O2DeckOfPlayingCards
let numberOfCards = 10
var deck = O2Deck.standard52CardDeck()
deck.shuffle()
for _ in 0..<numberOfCards {
guard let card = deck.deal() else {
print("No More Cards!")
break
}
print(card)
}

一般来说,一个包在根目录中包含一个命名为 main.swift 的文件生成一个可执行文件。

运行 swift build 命令开始用 Swift 编译来生成可以被运行在 .build/debug 目录下的可执行文件 O2Dealer

注:这里测试的时候会报错没有子模块,需要再运行 swift build 就可以通过了。原因不知道是不是因为没有加证书,这里我们先编译,后续再查找原因。

1
2
3
4
5
6
7
8
9
10
11
12
$ swift build
$ ./.build/debug/O2Dealer
♠︎6
♢K
♢2
♡8
♠︎7
♣︎10
♣︎5
♢A
♡Q
♡7

完整关于 O2Dealer 代码可以在如下地址查阅 https://github.com/marklin2012/O2Dealer.git

更多关于 SPM 信息查看文档: Swift package Manager project on GitHub

总结与展望

到此,我们已经完整地对 SPM 有了开发的经验了!如果你有过 NodeJS 的开发经历,你也许会有似曾相似的感觉,但我们用的可是 Swift!

Swift 是一门先进的语言, SPM 的社区也在不断地完善中。在 Swift 开源之后,我们很容易可以看到它的潜力,看来掌握这门语言必将是一个大趋势。而实践完这篇文章,我们已经快人一步啦!