使用SPM管理swift项目依赖库
概念概述
Modules模块
在 Swift 中我们使用模块来管理代码,每个模块指定一个命名空间并强制指定模块外哪些部分的代码是可以被访问控制的。
一个程序可以将它所有代码聚合在一个模块中,也可以将它作为依赖关系导入到其他模块。除了少量系统提供的模块,像 OS X 中的 Darwin 或者 Linux 中的 Glibc 等的大多数依赖需要代码被下载或者内置才能被使用。
当你将编写的解决特定问题的代码独立成一个模块时,这段代码可以在其他情况下被重新利用。例如,一个模块提供了发起网络请求的功能,在一个照片分享的 app 或者 一个天气的 app 里它都是可以使用的。使用模块可以让你的代码建立在其他开发者的代码之上,而不是你自己去重复实现相同的功能。
Packages包
一个包由 Swift 源文件和一个清单文件组成。这个清单文件称为 Package.swift,定义包名或者它的内容使用PackageDescription 模块。
一个包有一个或者多个目标,每个目标指定一个产品并且可能声明一个或者多个依赖。
import PackageDescription
let package = Package(
name: "DeckOfPlayingCards",
products: [
.library(name: "DeckOfPlayingCards", targets: ["DeckOfPlayingCards"]),
.executable(name: "Dealer", targets: ["Dealer"]),
],
dependencies: [
.package(url: "https://github.com/apple/example-package-fisheryates.git", from: "2.0.0"),
.package(url: "https://github.com/apple/example-package-playingcard.git", from: "3.0.0"),
],
targets: [
.target(
name: "DeckOfPlayingCards",
dependencies: ["FisherYates", "PlayingCard"]),
.testTarget(
name: "DeckOfPlayingCardsTests",
dependencies: ["DeckOfPlayingCards"]),
]
)
Products产品
一个target可能构建一个.library()库或者一个.executable()可执行文件作为其产品。
.library(name: "", targets: [])库:是包含用于其他Swift 代码导入该模块
.executable(name: "", targets: [])可执行文件:是一段可以被操作系统运行的程序。
Dependencies依赖
Dependencies依赖是指Package中代码必须添加的Modules块。Dependencies由.package资源的绝对路径或相对 URL 和包的版本组成。
包管理器的作用是通过自动为工程下载和编译所有依赖的过程中,减少协调的成本。这是一个递归的过程:依赖能有自己的依赖,其中每一个也可以具有依赖,形成了一个依赖相关图。
用例
创建依赖库lib Package
- 建一个
target表示标准的52张扑克牌的PlayingCard:
mkdir PlayingCard
cd PlayingCard
swift package init
- 创建公共实现类
PlayingCard定义PlayingCard类型,它由Suit枚举值(梅花、方块、红心、黑桃)和Rank枚举值(Ace、2、3、…、Jack、Queen、King)组成。
public enum Rank : Int {
case Ace = 1
case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King
}
public enum Suit: String {
case Spades, Hearts, Diamonds, Clubs
}
public struct PlayingCard {
let rank: Rank
let suit: Suit
}
按照约定,一个target所有的源文件都存在相应的Sources/<target-name>目录下,默认情况下,库模块使用public来声明类型和方法,这些类型和方法位于Sources/<target-name>中:
example-package-playingcard
├── Sources
│ └── PlayingCard
│ ├── PlayingCard.swift
│ ├── Rank.swift
│ └── Suit.swift
└── Package.swift
PlayingCard因为不生成executable可执行文件,这种类型的target成为库,当构建一个模块可以被依赖导入使用。
- 运行
swift build命令,将编译生成PlayingCard模块。
使用 #if #else #endif构建配置语句
- 创建
fisher模块
mkdir fisher
cd fisher
swift package init
fisher与PlayingCard不同,此模块不定义任何新类型。相反,它扩展了现有的类型(特别是CollectionType和MutableCollectionType协议),以添加shuffle()方法和它的变体副本shuffleInPlace()。
- 使用构建配置语句
#if #else #endif
shuffleInPlace()的实现使用Fisher-Yates算法随机排列集合中的元素。因为Swift标准库不提供随机数生成器,所以该方法必须调用从系统模块导入的函数。为了使该函数与macOS和Linux兼容,代码使用构建配置语句。
在macOS中,系统模块是Darwin,它提供了arc4random_uniform(_:)函数。
在Linux中,系统模块为Glibc,它提供了random()函数:
#if os(Linux)
import Glibc
#else
import Darwin.C
#endif
public extension MutableCollectionType where Index == Int {
mutating func shuffleInPlace() {
if count <= 1 { return }
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
if i == j { continue }
swap(&self[i], &self[j])
}
}
}
依赖库导入及使用
DeckOfPlayingCards包将前两个包组合在一起:它定义了一种桥牌类型,该类型在一个PlayingCard值数组上使用fisher()方法。
要使用fisher和PlayingCards模块,必须在DeckOfPlayingCards模块的清单文件Package.swift中声明为它们package依赖项。
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "DeckOfPlayingCards",
products: [
.library(name: "DeckOfPlayingCards", targets: ["DeckOfPlayingCards"]),
],
dependencies: [
.package(url: "https://github.com/apple/example-package-fisheryates.git", from: "2.0.0"),
.package(url: "https://github.com/apple/example-package-playingcard.git", from: "3.0.0"),
],
targets: [
.target(
name: "DeckOfPlayingCards",
dependencies: ["FisherYates", "PlayingCard"]),
.testTarget(
name: "DeckOfPlayingCardsTests",
dependencies: ["DeckOfPlayingCards"]),
]
)
每个依赖项指定源URL和from:版本号。
源URL是解析到Git存储库的当前用户可以访问的URL。版本需求遵循语义版本控制(SemVer)约定,用于确定签出哪个Git标签并使用它来构建依赖关系。对于依赖项FisherYates ,将使用最新版本主版本为2(例如,2.0.4)。类似地,PlayingCard依赖项将使用最新版本主版本为3。
当运行swift build命令时,包管理器下载所有依赖项,编译、并链接到包模块。这样DeckOfPlayingCards使用import语句来访问依赖模块中public类型的属性和方法。
您可以在项目根目录下的.build/checkouts目录中看到下载的源代码,在项目根目录下的.build目录中看到中间的构建产品。
解决依赖传递关系
构建Dealer模块,它依赖于DeckOfPlayingCards包,而DeckOfPlayingCards包又依赖于PlayingCard和fisher包。然而,由于Swift包管理器自动解析传递依赖项,您只需将DeckOfPlayingCards包声明为依赖项即可。
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "dealer",
products: [
.executable(name: "Dealer", targets: ["Dealer"]),
],
dependencies: [
.package(url: "https://github.com/apple/example-package-deckofplayingcards.git", from: "3.0.0"),
],
targets: [
.target(
name: "Dealer",
dependencies: ["DeckOfPlayingCards"]),
]
)
依赖传递关系也体现在Swift源文件代码import导入一个模块,就可以使用该某块下所有依赖库的类型。例如Dealer模块的main.swift文件。在DeckOfPlayingCards中的Deck类型和PlayingCard中的PlayingCard类型。尽管Deck类型的shuffle()方法在内部使用了fisher模块,但该模块不需要在main.swift中导入。
import PlayingCard
import DeckOfPlayingCards
let numberOfCards = 10
var deck = Deck.standard52CardDeck()
deck.shuffle()
for _ in 1...numberOfCards {
guard let card = deck.deal() else {
print("No More Cards!")
break
}
print(card)
}
构建并运行可执行文件
根据约定,一个target的根目录中包含一个名为main.swift文件,可以构建成一个可执行文件。
运行swift build命令,然后运行.build/debug目录下的Dealer可执行文件。
$ swift build
$ ./.build/debug/Dealer
︎6
K
2
8
︎7
︎10
︎5
A
Q
7
两个坑
swift package generate-xcodeproj
- 每次更新或添加库或框架,xcodeproj就需要重新创建一次,不然无法引用到新库或新框架;
- 在source文件夹如果发生任何改动,库或框架的更新就会失败。