1.1 快速上手

原文链接: https://docs.swift.org/latest/documentation/packagemanagerdocs/gettingstarted/

1.1 快速上手

学习创建和使用 Swift 包。

概述

为了更完整地展示 Swift Package Manager 能做什么,下面的例子由三个相互依赖、可供你自行探索的包组成:

  • PlayingCard —— 定义 PlayingCard、Suit 和 Rank 类型。
  • DeckOfPlayingCards —— 定义 Deck 类型,用于洗牌和发牌,牌组由 PlayingCard 值组成。
  • Dealer —— 定义一个可执行文件,它创建 DeckOfPlayingCards、洗牌,并发出前 10 张牌。

本指南会展示如何创建一个库、让它依赖另一个库,如何使用包管理器构建和测试代码,以及如何发布你自己的包。

创建库包

这个例子从使用一个已有包开始:它表示一副标准 52 张扑克牌中的一张。这个包 PlayingCard 通过 git 提供,其中的库(PlayingCard)是本指南使用并加以扩展的基础。

这个例子要创建的库 DeckOfPlayingCards 提供一个表示牌组的类型,以及对该牌组的常见操作,包括洗牌、计数和发牌。要新建一个库,先创建一个空目录,在其中运行 swift package init 命令来初始化新包:

1
2
3
mkdir DeckOfPlayingCards
cd DeckOfPlayingCards
swift package init

包管理器创建的默认模板是一个库,你可以用 --type 参数控制。包名默认取你创建目录的名字,也可以通过 swift package init 命令的 --name 参数覆盖。关于该命令选项的完整细节,参见 swift package init 文档。

模板会在目录中生成一套符合 Swift 包默认约定的文件结构:

1
2
3
4
5
6
7
8
9
DeckOfPlayingCards
├── .gitignore
├── Package.swift
├── Sources
│   └── DeckOfPlayingCards
│       └── DeckOfPlayingCards.swift
└── Tests
    └── DeckOfPlayingCardsTests
        └── DeckOfPlayingCardsTests.swift

模板在 Sources/DeckOfPlayingcards 提供了一个用于承载单个模块的目录(该模块作为库暴露出去),并提供一个与之对应的测试目录。默认的包结构提供一个由单个目标构成的库,库名与目标名都与包名相同(DeckOfPlayingCards),另外还有一个测试目标,你可以在开发代码的过程中把测试加进去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
let package = Package(
    name: "DeckOfPlayingCards",
    products: [
        // 产品定义包产出的可执行文件和库,使它们对其他包可见。
        .library(
            name: "DeckOfPlayingCards",
            targets: ["DeckOfPlayingCards"]
        ),
    ],
    targets: [
        // 目标是包的基本构建单元,定义一个模块或测试套件。
        // 目标可以依赖本包内的其他目标,以及依赖所提供的产品。
        .target(
            name: "DeckOfPlayingCards"
        ),
        .testTarget(
            name: "DeckOfPlayingCardsTests",
            dependencies: ["DeckOfPlayingCards"]
        ),
    ]
)

添加依赖

要让这个例子使用提供 PlayingCard 的库,就需要添加对它的依赖:使用 add-dependency 命令,并给出该包的托管位置。

1
swift package add-dependency https://github.com/apple/example-package-playingcard --from 3.0.0

包中的每个依赖都要指定源 URL 和版本要求。源 URL 是当前用户可访问、且能解析到某个 Git 仓库的 URL。包管理器用这些遵循语义化版本(SemVer)约定的版本要求来决定检出并使用哪个 Git 标签来构建该依赖。

上面的例子使用参数 --from 3.0.0 来说明该依赖的版本要求:from 把选定的依赖限制在最低 3.0.0,最高可到该 Git 仓库中可用的最高次版本和补丁版本。包管理器用标签(并将其解释为语义化版本)来确定可用的版本。该命令更新了 Package.swift 清单文件,加入 dependencies 一节:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let package = Package(
    name: "DeckOfPlayingCards",
    products: [
        ...
    ],
    dependencies: [
        .package(url: "https://github.com/apple/example-package-playingcard", from: "3.0.0"),
    ],
    targets: [
        ...
    ]
)

添加依赖只是让该依赖对包可用,默认并不会把它包含进包内的目标中。例如,如果你尝试用 swift build 构建这个包,构建会成功,但会给出警告:

1
warning: 'deckofplayingcards': dependency 'example-package-playingcard' is not used by any target

如果你试图在源代码中使用这个库,例如写 import PlayingCard,编译器会报告 No such module 'PlayingCard'。

运行 swift build 命令时,包管理器会下载全部依赖、编译它们,并根据 Package.swift 清单把它们链接到包模块上。你可以在项目根目录的 .build/checkouts 目录中看到下载下来的源码,在项目根目录的 .build 目录中看到中间构建产物。

你还需要在想要使用该库的目标上加入这一依赖。使用 add-target-dependency 命令可以把该依赖加到本包中的目标上。

1
swift package add-target-dependency PlayingCard DeckOfPlayingCards --package example-package-playingcard

这样 DeckOfPlayingCards 就能通过 import 语句访问其依赖模块的公开成员。上面的命令会更新 Package.swift 清单,使 DeckOfPlayingCards 目标引用该依赖:

1
2
3
4
5
6
.target(
    name: "DeckOfPlayingCards",
    dependencies: [
        .target(name: "PlayingCard"),
    ]
),

完成这一修改后,再运行 swift build,包就能无警告地通过编译。

实现这个库

模板为你的包源代码提供了一个空文件。清空其内容,加上 import PlayingCard,然后写你的实现。下面的代码给出一个示例实现:

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import PlayingCard

/// 用于洗牌和发牌的扑克牌组模型。
///
/// 一副扑克牌由 52 张牌组成,分属四个花色:黑桃、红桃、方块和梅花。
/// 点数有 13 种:从 2 到 10,然后是 J、Q、K 和 A。
public struct Deck: Equatable {
  fileprivate var cards: [PlayingCard]

  /// 返回一副扑克牌。
  public static func standard52CardDeck() -> Deck {
    var cards: [PlayingCard] = []
    for rank in Rank.allCases {
      for suit in Suit.allCases {
        cards.append(PlayingCard(rank: rank, suit: suit))
      }
    }

    return Deck(cards)
  }

  /// 创建一副扑克牌。
  public init(_ cards: [PlayingCard]) {
    self.cards = cards
  }

  /// 随机打乱一副扑克牌。
  public mutating func shuffle() {
    cards.shuffle()
  }

  /// 从牌组中发一张牌。
  ///
  /// 该函数返回牌组中的最后一张牌。
  public mutating func deal() -> PlayingCard? {
    guard !cards.isEmpty else { return nil }

    return cards.removeLast()
  }

  /// 牌组中剩余的牌数。
  public var count: Int {
    cards.count
  }
}

// MARK: - ExpressibleByArrayLiteral

extension Deck: ExpressibleByArrayLiteral {
  public init(arrayLiteral elements: PlayingCard...) {
    self.init(elements)
  }
}

用 swift build 构建这个包,用 swift test 运行与该包关联的全部测试。默认模板还包含默认使用 swift-testing 的结构,其中包含一个空白但可用的测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[1/1] Planning build
Building for debugging...
[10/10] Linking DeckOfPlayingCardsPackageTests
Build complete! (2.68s)
Test Suite 'All tests' started at 2025-10-09 13:12:08.094.
Test Suite 'All tests' passed at 2025-10-09 13:12:08.095.
     Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds
◇ Test run started.
↳ Testing Library Version: 1400
↳ Target Platform: arm64e-apple-macos14.0
◇ Test example() started.
✔ Test example() passed after 0.001 seconds.
✔ Test run with 1 test in 0 suites passed after 0.001 seconds.

为你的包添加测试

扩展测试,让它们能验证你库中的代码。更新 Tests/DeckOfPlayingCardsTests/DeckOfPlayingCardsTests.swift 的内容:

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import DeckOfPlayingCards
import PlayingCard
import Testing

struct DeckTests {
  @Test
  func standard52CardDeck() {
    var countByPlayingCard: [PlayingCard: Int] = [:]

    var deck = Deck.standard52CardDeck()
    while let playingCard = deck.deal() {
      countByPlayingCard[playingCard, default: 0] += 1
    }

    #expect(countByPlayingCard.count == 52)
    #expect(countByPlayingCard.values.allSatisfy({ $0 == 1 }))

    for rank in Rank.allCases {
      for suit in Suit.allCases {
        let playingCard = PlayingCard(rank: rank, suit: suit)
        #expect(countByPlayingCard[playingCard] == 1)
      }
    }
  }

  @Test
  func deal() {
    let playingCard = PlayingCard(rank: .ace, suit: .clubs)
    var deck: Deck = [playingCard]

    #expect(deck.deal() == playingCard)
    #expect(deck.deal() == nil)
  }

  @Test
  func countEmptyDeckHasZeroCards() {
    let deck = Deck()
    //XCTAssertEqual(deck.count, 0)
    #expect(deck.count == 0)
  }

  @Test
  func countStandard52CardDeckHas52Cards() {
    let deck = Deck.standard52CardDeck()

    #expect(deck.count == 52)
  }

  @Test
  func countDealingDecreasesCountByOne() throws {
    var deck = Deck([
      PlayingCard(rank: .ace, suit: .spades), PlayingCard(rank: .queen, suit: .hearts),
    ])

    #expect(deck.count == 2)
    try #require(deck.deal() != nil)
    #expect(deck.count == 1)
  }
}

之后再运行测试,就能看到每个测试及其结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Building for debugging...
[6/6] Linking DeckOfPlayingCardsPackageTests
Build complete! (0.44s)
Test Suite 'All tests' started at 2025-10-09 13:16:44.052.
Test Suite 'All tests' passed at 2025-10-09 13:16:44.052.
     Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds
◇ Test run started.
↳ Testing Library Version: 1400
↳ Target Platform: arm64e-apple-macos14.0
◇ Suite DeckTests started.
◇ Test deal() started.
◇ Test countStandard52CardDeckHas52Cards() started.
◇ Test countEmptyDeckHasZeroCards() started.
◇ Test standard52CardDeck() started.
◇ Test countDealingDecreasesCountByOne() started.
✔ Test countDealingDecreasesCountByOne() passed after 0.001 seconds.
✔ Test deal() passed after 0.001 seconds.
✔ Test countStandard52CardDeckHas52Cards() passed after 0.001 seconds.
✔ Test countEmptyDeckHasZeroCards() passed after 0.001 seconds.
✔ Test standard52CardDeck() passed after 0.001 seconds.
✔ Suite DeckTests passed after 0.001 seconds.
✔ Test run with 5 tests in 1 suite passed after 0.001 seconds.

DeckOfPlayingCards 示例包的完整代码可以在 https://github.com/apple/example-package-deckofplayingcards 找到。

共享你的包

你可以在本地的其他 Swift 包中使用这个包,也可以通过 Git 托管服务共享它。当你想发布自己的包时,创建一个与语义化版本的主、次、补丁版本对应的 Git 标签,并把该标签推送到你的 Git 托管服务。例如,要用语义化版本 0.1(表示第一次次版本发布)给包打标签,就使用标签 0.1.0。

关于共享包的更多内容,参见发布与公开 Swift 包。

解析传递依赖

包管理器会解析你所用包及其所有传递依赖的依赖关系。另一个示例包 Dealer 通过使用本指南创建的包,展示了这一机制如何工作。你可以在线浏览这个示例包 https://github.com/swiftlang/example-package-dealer/,也可以把它下载到本地来探索:

1
2
git clone https://github.com/swiftlang/example-package-dealer.git
cd example-package-dealer

dealer 包额外依赖 Swift Argument Parser,这是一个帮助解析命令行应用参数的工具包。

要查看依赖解析的过程与选择结果,请运行命令 swift package resolve。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Fetching https://github.com/swiftlang/example-package-deckofplayingcards.git
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetched https://github.com/swiftlang/example-package-deckofplayingcards.git from cache (0.41s)
Fetched https://github.com/apple/swift-argument-parser.git from cache (0.58s)
Computing version for https://github.com/swiftlang/example-package-deckofplayingcards.git
Computed https://github.com/swiftlang/example-package-deckofplayingcards.git at 4.0.0 (0.94s)
Fetching https://github.com/apple/example-package-playingcard.git from cache
Fetched https://github.com/apple/example-package-playingcard.git from cache (0.30s)
Computing version for https://github.com/apple/example-package-playingcard.git
Computed https://github.com/apple/example-package-playingcard.git at 4.0.0 (0.66s)
Computing version for https://github.com/apple/swift-argument-parser.git
Computed https://github.com/apple/swift-argument-parser.git at 1.6.1 (0.39s)
Creating working copy for https://github.com/apple/swift-argument-parser.git
Working copy of https://github.com/apple/swift-argument-parser.git resolved at 1.6.1
Creating working copy for https://github.com/apple/example-package-playingcard.git
Working copy of https://github.com/apple/example-package-playingcard.git resolved at 4.0.0
Creating working copy for https://github.com/swiftlang/example-package-deckofplayingcards.git
Working copy of https://github.com/swiftlang/example-package-deckofplayingcards.git resolved at 4.0.0

当你运行 swift build 或 swift test 时,这一过程会自动发生,使依赖对你的项目可用。与前一个包一样,你可以用 swift build 构建这个包,并用命令 swift test 运行、查看该包的测试。

由于 dealer 包提供了一个命令行可执行文件,你还可以用 swift run 运行该包构建出来的可执行文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version-2C315BDEC41BFF30.txt
Build of product 'dealer' complete! (0.13s)
Error: Missing expected argument '<count> ...'

OVERVIEW: Shuffles a deck of playing cards and deals a number of cards.

For each count argument, prints a line of tab-delimited cards to stdout,
or if there aren't enough cards remaining,
prints "Not enough cards" to stderr and exits with a nonzero status.

USAGE: dealer <count> ...

ARGUMENTS:
  <count>                 The number of cards to deal at a time.

OPTIONS:
  -h, --help              Show help information.

指定可执行文件名以及所需的参数来试一试,例如 swift run dealer 5:

1
2
3
4
Building for debugging...
[1/1] Write swift-version-2C315BDEC41BFF30.txt
Build of product 'dealer' complete! (0.07s)
♢ J    ♢ 3    ♢ 7    ♣︎ 5    ♡ 7

包的构建产物默认也在 .build 目录中,你也可以在那里直接运行该工具。例如,dealer 包的调试构建(默认构建)位于 .build/debug/dealer,你可以在终端里调用它:.build/debug/dealer 5

1
♠︎ 6    ♡ 4    ♣︎ 4    ♡ A    ♡ K