Swift Package Manager 快速入门

Swift Package Manager(SPM)是 Swift 官方的构建与依赖管理工具。本文面向 iOS/Swift 开发者入门与进阶,把「创建包 / Xcode 集成 / 添加依赖 / 资源打包 / 命令行工具 / 常见坑」串成一条可直接照做的路径。

TL;DR(最常用命令)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建可执行包(命令行工具)
mkdir hello && cd hello
swift package init --type executable

# 构建/运行/测试
swift build
swift run
swift test

# 检查当前包信息(排错时很有用)
swift package describe
swift package dump-package

# 重新解析依赖(必要时)
swift package resolve

1. 新建一个可执行 Package(命令行工具)

1
2
mkdir hello && cd hello
swift package init --type executable

初始化后,默认结构通常是:

1
2
3
4
5
6
7
hello/
Package.swift
Sources/
hello/
main.swift
Tests/
helloTests/

你可以直接运行:

1
swift run

2. 先搞清楚:Target、Product、Dependency 是什么

  • Dependency:你依赖的“外部包”(例如 Alamofire 的 Git 仓库)
  • Target:你自己包里的“模块”(源码组织单位,能被其他 target 依赖)
  • Product:对外暴露出来可被别人使用的产物(library / executable),通常由 target 组合而来

常见误区是把“外部依赖包名”当作可以直接写进 dependencies: [...] 的名字。更稳妥的方式是 依赖它的 product

1
.product(name: "Alamofire", package: "Alamofire")

3. 用 Xcode 打开与运行(推荐方式)

现在一般不需要生成 .xcodeproj直接用 Xcode 打开 Package.swift 即可 Run / Debug(或在目录里执行 xed .)。

旧命令 swift package generate-xcodeproj 已经逐渐退出主流工作流;建议直接用 Xcode 打开 package。

4. 在 Xcode 工程里添加 SPM 依赖(两条路)

4.1 通过 Xcode UI 添加(最常见)

两种入口等价(也支持本地 package,选择时点 Add Local):

  • Xcode → File → Add Packages…
  • Project Settings → Project → Package Dependencies → “+”

4.2 把依赖写回 Package.swift(更可控)

当你的工程本身也是一个 package(或你在维护一个库)时,把依赖写进 Package.swift 更容易 review 与回滚。

5. Package.swift:一个“可复制粘贴就能跑”的示例

下面示例匹配默认目录结构(Sources/<TargetName>/...),并演示:

  • 一个可执行 target:Hello
  • 一个内部模块:Core
  • 一个外部依赖:Alamofire(用 .product(...) 引入)
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
// swift-tools-version: 5.8
import PackageDescription

let package = Package(
name: "Hello",
platforms: [
.macOS(.v13)
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.4"))
],
targets: [
.executableTarget(
name: "Hello",
dependencies: [
"Core",
.product(name: "Alamofire", package: "Alamofire")
]
),
.target(
name: "Core"
),
.testTarget(
name: "CoreTests",
dependencies: ["Core"]
)
]
)

对应的建议目录结构:

1
2
3
4
5
6
7
Sources/
Hello/
main.swift
Core/
Core.swift
Tests/
CoreTests/

如果你要做的是 iOS App/Framework 的依赖包,通常会把 .executableTarget 换成 .target / .library,并把 platforms 改成 .iOS(.vXX)(或同时支持 iOS/macOS)。

6. 给 Swift Package 添加资源(Resources)与 Bundle.module

SwiftPM 支持把资源打进 bundle(图片、json 等)。典型做法是把资源放进某个目录(例如 Sources/Core/Resources),然后在 target 配置 resources

1
2
3
4
5
6
.target(
name: "Core",
resources: [
.process("Resources")
]
)

读取资源时用 Bundle.module(只在 package 内可用):

1
2
3
import Foundation

let url = Bundle.module.url(forResource: "config", withExtension: "json")

参考:https://useyourloaf.com/blog/add-resources-to-swift-packages/

7. 常见问题与排错(按优先级)

7.1 Xcode/命令行拉依赖很慢或失败(代理/网络)

某些网络环境下 Xcode 拉取 GitHub 依赖不稳定,可以先用命令行把依赖 resolve 掉:

1
2
3
4
# 按你本机代理端口调整(常见还有 http_proxy / all_proxy)
export https_proxy=http://127.0.0.1:1234

xcodebuild -resolvePackageDependencies

如果 SPM 依赖是“挂在某个 xcode project”上,可以指定 project + scheme:

1
xcodebuild -resolvePackageDependencies -project Kickstarter.xcodeproj -scheme Kickstarter-iOS

如果你的目的是让 xcodebuild -resolvePackageDependencies 走代理,更可靠的方式通常是给“这一次命令”设置代理环境变量(避免污染全局 git 配置):

1
2
3
4
5
6
7
8
# HTTP 代理(按你本机代理端口调整)
http_proxy=http://127.0.0.1:7890 \
https_proxy=http://127.0.0.1:7890 \
xcodebuild -resolvePackageDependencies

# SOCKS5 代理(如你的代理软件提供的是 socks5)
ALL_PROXY=socks5://127.0.0.1:7890 \
xcodebuild -resolvePackageDependencies

另外在部分环境下,-scmProvider system 会更稳定(强制使用系统的 SCM provider):

1
xcodebuild -resolvePackageDependencies -scmProvider system

7.2 xcodebuild 找不到 Xcode(CommandLineTools 被选中)

报错类似:

1
xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance

执行:

1
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

7.3 Package.resolved schema/version 不兼容

当 Xcode 与 SwiftPM 的 Package.resolved 格式不兼容时,最简单的处理通常是 删除并重新解析

常见位置:

  • 在 package 仓库里.swiftpm/Package.resolved(或项目根目录的 Package.resolved,取决于工具链/项目类型)
  • 在 Xcode 工程/工作区里Project.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

删除后再执行:

1
2
3
swift package resolve
# 或
xcodebuild -resolvePackageDependencies

7.4 Realm 产品名冲突(multiple products named ‘Realm’ / ‘RealmSwift’)

报错类似:

1
2
multiple products named 'Realm' in: 'realm-cocoa', 'realm-swift'
multiple products named 'RealmSwift' in: 'realm-cocoa', 'realm-swift'

常见原因是 Realm 仓库地址调整导致的重复依赖。把旧 URL 改为新仓库即可:

1
2
3
4
// 旧:
.package(name: "Realm", url: "https://github.com/realm/realm-cocoa", from: "10.5.0")
// 新:
.package(name: "Realm", url: "https://github.com/realm/realm-swift", from: "10.5.0")
Realm repo rename

8. “更新依赖”与清缓存(谨慎使用)

命令行没有一个“万能的一键更新所有依赖”的命令;通常是“更新版本约束 + 重新 resolve”。当你遇到缓存/checkout 异常,才考虑清缓存。

建议先从轻到重:

  • 在 Xcode 里 Reset Package Caches(通常在 File → Packages → Reset Package Caches
  • 删除 Package.resolved 并重新 resolve
  • 最后再清理 DerivedData 与 SwiftPM 缓存(会比较重)

一个“重置到能重新拉依赖”的命令集:

1
2
3
4
5
6
7
rm -rf ~/Library/Developer/Xcode/DerivedData/ \
~/Library/Caches/org.swift.swiftpm/ \
~/Library/org.swift.swiftpm/

rm -rf ProjectName.xcodeproj/project.xcworkspace/xcshareddata/swiftpm

xcodebuild -resolvePackageDependencies

9. 命令行工具 Keep Alive(避免异步任务没跑完就退出)

命令行程序跑到 main.swift 末尾就会退出,异步任务可能还没完成。常见做法是在末尾 keep alive:

1
2
3
RunLoop.main.run() // 或 dispatchMain()

// 在异步完成时调用 exit(EXIT_SUCCESS) 结束程序

10. 常用路径速查(理解“依赖到底存哪儿了”)

  • SwiftPM 缓存(git repo 存储):~/Library/Caches/org.swift.swiftpm/
  • Xcode DerivedData 下依赖 checkout:~/Library/Developer/Xcode/DerivedData/ProjectName-xxxx/SourcePackages

参考链接(延伸阅读)

  • Package Collections:https://swift.org/blog/package-collections/
  • Apple 文档:https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app
  • Guide:
    • https://www.raywenderlich.com/1993018-an-introduction-to-swift-package-manager
    • https://www.swiftbysundell.com/articles/building-a-command-line-tool-using-the-swift-package-manager/
    • https://www.fivestars.blog/articles/ultimate-guide-swift-executables/
    • https://stackoverflow.com/questions/31944011/how-to-prevent-a-command-line-tool-from-exiting-before-asynchronous-operation-co
    • https://forums.swift.org/t/xcode-environment-variables/45249
    • https://appcoda.com.tw/swift-package-manager/