Swift Package Manager(SPM)私有组件完整实操
适用环境:Xcode 27(内置 Swift 6.4)/ macOS / 私有 Git 托管(Gitee / GitLab / 企业自建) 说明:文中
git@<host>:命名空间/仓库名.git、/Users/你的用户名/...、https://github.com/某用户/某仓库.git均为示例占位符,请替换为真实地址与路径。 阅读路径:① Package.swift 是什么/怎么用 → ② 本地如何使用 → ③ 远程如何使用 → ④ 远程使用踩坑汇总 → ⑤ 附录
第〇部分:整体流程图
[创建SPM包] → [本地调试] → [推私有Git] → [打Tag] → [配置SSH] → [Xcode远程添加] → [关联Target] → [import编译]
│ │ │ │ │ │
│ └── 实时改代码立即生效 │ └── 90%失败都在这
└── Package.swift放仓库根目录 └── 必须打语义化tag(纯数字)
第一部分:Package.swift 是什么 & 如何使用
1.1 一句话定位
Package.swift 是 Swift Package Manager 的包描述文件(清单文件),位于仓库根目录。SPM 读取它来获知:包的名称、编译哪些源码模块(target)、对外暴露哪些库(product)、依赖哪些第三方包、最低平台版本、语言版本等。它是 SPM 能否识别这个仓库为 Swift 包的唯一入口。
结构本质:就是一段 Swift 代码,调用 SPM 内置的
PackageDescription库,构建一个Package(...)配置对象。
1.2 核心概念:Product vs Target(90% 混淆点)
| 概念 | 作用域 | 说明 |
|---|---|---|
| Product(产物) | 对外出口 | 外部 Xcode 项目/其他包只能 import products 里声明的库 |
| Target(编译单元) | 包内部 | 包内独立模块,可 Swift/OC 混编,可互相依赖;不放进 product 外部就看不到 |
类比:Target = 车间零件;
.library()= 打包成成品盒对外发货;name= 盒子名(外部 import 写的名),targets= 装哪些零件。
1.3 products / .library 详解
.library(
name: "MyComponent", // 产物名 → 外部 import MyComponent 写的就是它
targets: ["MyComponent"] // 由哪些内部 target 组成这个库(可多个)
)
name:对外暴露的模块名,外部import XXX写的就是这个名字。targets:数组,可放多个 target 合并成一个对外库。type:可选,.static(静态)/.dynamic(动态),不写 SPM 自动推断,iOS 私有组件一般省略即可。
1.4 targets / .target 详解
.target(
name: "MyComponent", // target 名,默认对应 Sources/MyComponent 目录
dependencies: ["Helpers"], // 依赖同包内其他 target
path: "Sources/Helpers", // 可选:自定义源码根目录(默认 Sources/<name>)
sources: ["src"], // 可选:只编译指定子目录的源码
publicHeadersPath: "include" // OC 混编:公开头文件目录
)
- 不写
path时:SPM 默认到Sources/<target名>找源码,文件夹名必须和 target 名一致。 path/sources是自定义配置,适合存量 OC 代码迁移;简单组件不建议自定义,目录对不上就编译失败。publicHeadersPath:OC 混编必备,目录下.h是公开头;私有头不要放这里。testTarget:单元测试模块,仅本地测试用,打包给外部时不会引入。
1.5 顶层常用字段
| 字段 | 作用 | 备注 |
|---|---|---|
name | 包名 | 推荐与仓库名/产物名一致,大小写统一 |
platforms | 最低支持平台版本 | 建议必写(如 .iOS(.v15)),不写高版本 Xcode 易解析异常 |
products | 对外产物列表 | 需要暴露几个模块就写几个 .library |
dependencies | 依赖的其他 SPM 包 | 本地/远程包均可 |
targets | 内部编译单元列表 | 含业务 target + testTarget |
swiftLanguageModes | Swift 语言版本 | 兼容旧代码可设 [.v5] |
1.6 Package.swift 的几种使用方式(总览)
| 使用方式 | 本质 | 适用场景 |
|---|---|---|
| ① 本地路径引入 | 依赖本地文件夹,不依赖 Git | 开发调试阶段,实时改代码 |
| ② 远程 Git 引入 | 依赖 Git 仓库 + Tag | 团队正式私有组件、CI |
| ③ 二进制 XCFramework | .binaryTarget,闭源 SDK | 不想暴露源码的交付 |
下文第二、三部分分别讲 ①②,二进制在附录简述。
1.7 校验合法性
cd <包根目录>
swift package describe # 快速验证语法;错误会直接提示
swift package dump-package # 输出完整 JSON,看结构是否符合预期
describe只能在本地 Package 目录执行,不支持直接拉远程(见踩坑 6)。
第二部分:创建 SPM 包 & 本地使用
2.1 命令行创建
mkdir MyComponent && cd MyComponent
swift package init --type library # --type library 代码库;executable 可执行程序
2.2 标准目录结构(尽量遵守,少踩坑)
MyComponent/
├─ Package.swift
├─ Sources/
│ ├─ MyComponent/ ← 和 target 名一致
│ │ └─ *.swift
│ └─ Helpers/ ← OC target(若混编)
│ ├─ src/ ← .m 实现
│ └─ include/ ← .h 公开头
└─ Tests/
└─ MyComponentTests/
2.3 最小可用 Package.swift(纯 Swift 单模块,推荐)
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MyComponent",
platforms: [.iOS(.v15)],
products: [
.library(name: "MyComponent", targets: ["MyComponent"])
],
targets: [
.target(name: "MyComponent", dependencies: []),
.testTarget(name: "MyComponentTests", dependencies: ["MyComponent"])
],
swiftLanguageModes: [.v5]
)
2.4 Swift + Objective-C 混编模板(对应含 Helpers 的场景)
import PackageDescription
let package = Package(
name: "MyComponent",
platforms: [.iOS(.v15)],
products: [
.library(name: "MyComponent", targets: ["MyComponent"])
],
targets: [
.target(
name: "MyComponent",
dependencies: ["Helpers"]
),
.target(
name: "Helpers",
path: "Sources/Helpers",
sources: ["src"],
publicHeadersPath: "include"
),
.testTarget(
name: "MyComponentTests",
dependencies: ["MyComponent"]
),
],
swiftLanguageModes: [.v5]
)
说明:
MyComponent(Swift) 依赖Helpers(OC);publicHeadersPath: "include"下的.h才是公开头,Swift 可直接#import,无需桥接文件。Helpers 未放进 product,外部 App 不能直接import Helpers。
2.5 本地使用方式(Add Local)
- Xcode → Project → Package Dependencies →
+→ Add Local(添加本地包) - 选中
MyComponent文件夹 - 在需要使用的 Target → Build Phases → Link Binary With Libraries →
+添加该库 - 代码
import MyComponent
✅ 优点:无需 Git,修改组件源码主工程实时生效,开发调试首选。 ⚠️ 缺点:只在本机有效,团队共享需每人配本地路径。
第三部分:远程使用(推送私有 Git + Xcode 引入)
3.1 前置准备:SSH 密钥(最重要,90% 失败在这)
cat ~/.ssh/id_ed25519.pub # 查看;No such file 则先生成
ssh-keygen -t ed25519 -C "你的托管平台登录邮箱" # 一路回车;passphrase 建议留空
- 公钥加到 Git 托管账号:设置 → 安全设置 → SSH 公钥
- 私钥加载进 ssh-agent:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519 - 测试连通:
ssh -T git@<你的host> # 首次输入 yes;成功输出 authenticated
3.2 推送仓库 & 打 Tag
git init && git add . && git commit -m "init"
git remote add origin git@<host>:命名空间/仓库名.git
git push -u origin main
git tag 1.0.0 && git push origin 1.0.0 # 【重点】必须打语义化tag,纯数字不带 v
验证远程 tag:
git ls-remote --tags git@<host>:命名空间/仓库名.git
3.3 Xcode 27 添加远程私有 SPM(正确入口)
- 入口一(推荐):
File → Add Package Dependency…(不是 Add Package Collection) - 入口二:项目根节点 → 右侧 Package Dependencies 标签 → 底部
+
弹窗内操作:
- 左侧保持 Recently Used(不要选中 Collections)
- 右上角输入框粘贴 SSH 地址:
git@<host>:命名空间/仓库名.git❗ 不要 HTTPS(需 PAT 令牌,会过期)
- 回车,版本规则:
- 生产:
Version - Up to Next Major Version+1.0.0 - 临时调试:
Branch→main(绕开 tag 逻辑,便于定位问题)
- 生产:
- 勾选要链接的 App Target(务必勾选!否则 No such module)
- Add Package →
import MyComponent
第四部分:远程使用遇到的问题总结(踩坑全集)
坑 1:Received invalid response ... package collection URL
- 根因:点错入口——左侧选中了
Collections(包集合索引,不能填 Git 地址)。 - 解决:切回
Recently Used,再粘贴 SSH 地址。
坑 2:ssh -T git@<host> → Permission denied (publickey)
- 根因:公钥未加到账号,或密钥不匹配。
- 解决:复制
cat ~/.ssh/id_ed25519.pub内容 → 平台 SSH 公钥页粘贴 → 重测。
坑 3:cat ~/.ssh/id_ed25519.pub: No such file or directory
- 根因:本机从未生成 ed25519 密钥。
- 解决:
ssh-keygen -t ed25519 -C "邮箱"一路回车生成。
坑 4:Passphrase required to unlock SSH key
- 根因:私钥设置了 passphrase。
- 解决:点
Unlock Key…输密码;或ssh-add --apple-use-keychain记住;调试期建议留空。
坑 5:unexpectedly did not find the new dependency in the package graph
- 根因 A:仓库根目录没有 Package.swift(SPM 强制从根目录读取,子目录不自动查找)→ 报
/Package.swift doesn't exist。 - 根因 B:Xcode 内置 SCM 与系统 Git 独立,内置 SCM 不认系统 known_hosts 指纹,fetch 成功但解析静默失败。
- 根因 C:包 identity 大小写不一致。
- 解决顺序:
- 确认仓库根目录存在 Package.swift(git clone 后
swift package describe校验); - 确认该 tag 对应 commit 根目录有 Package.swift(旧 tag 常见问题);
- 关闭 Xcode 内置 SCM:
defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM NO - 清理缓存 + 重启 + 删失败依赖 + 重加(先用 Branch 测试)。
- 确认仓库根目录存在 Package.swift(git clone 后
坑 6:swift package describe --package-url → Unknown option
- 根因:
describe只能在本地包目录执行,不支持直接拉远程。 - 解决:先
git clone到本地,再在包目录swift package describe / dump-package。
坑 7:No such module 'MyComponent'(依赖在列表,代码 import 不到)
- 根因:依赖已添加但未关联 App Target。
- 解决:Target → Build Phases → Link Binary With Libraries →
+添加;或添加包最后一步勾选 Target。之后File → Packages → Reset Package Caches → Resolve Package Versions→ 重新编译。
坑 8:删除了 SPM 库,Xcode 里还在 / 找不到删除按钮
- 根因:点的是 Cancel(只取消新增),或点开了包内 Package.swift 源码视图(看不到管理面板)。
- 正确删除:项目根节点 → 右侧 Package Dependencies → 选中包 → 底部减号
-→ Delete。 - 兜底(关闭 Xcode 后):
cd /Users/你的用户名/工程目录/工程名 rm -rf 工程名.xcworkspace/xcshareddata/swiftpm rm -rf ~/Library/Caches/org.swift.swiftpm rm -rf ~/Library/Developer/Xcode/DerivedData
坑 9:the package manifest at '/Package.swift' cannot be accessed
- 根因:仓库根目录无 Package.swift,或该 tag 对应 commit 没有该文件。
- 解决:把 Package.swift 移到仓库根目录并提交,重打 tag(旧 tag 不会自动更新缓存)。
第五部分:命名规范(规避包图异常)
| 名称 | 是否必须一致 | 说明 |
|---|---|---|
| 仓库文件夹名 / Git 仓库名 | 不强制,强烈建议与包名一致 | 大小写混用易触发 identity 异常 |
Package(name:) 包名 | 主标识 | 建议统一 |
.library(name:) 产物名 | 外部 import 的名 | 决定 import XXX 写什么 |
.target(name:) | 默认对应 Sources/<名> | 不自定义 path 时必须一致 |
| import 名 | = .library(name:) | 严格大小写一致 |
反面例子(易报错):包名
MyComponent,仓库名mycomponent,大小写不一致 →did not find the new dependency in the package graph。
第六部分:三种完整 Package.swift 示例
示例一:纯 Swift 组件(单模块 + 单元测试)
最简单、最常见的源码组件模板。适用于:纯 Swift 逻辑、网络层、工具类等不含 OC 的模块。
// swift-tools-version:5.9
import PackageDescription
let package = Package(
// 包名,建议与仓库名、产物名一致
name: "MyComponent",
// 最低支持平台(必写,避免高版本 Xcode 解析异常)
platforms: [
.iOS(.v15),
.macOS(.v13)
],
// 对外暴露的产物:外部项目 import 的名字 = 这里 library 的 name
products: [
.library(
name: "MyComponent",
targets: ["MyComponent"]
),
],
// 依赖的其他 SPM 包(本地/远程都可,无依赖则留空数组)
dependencies: [
// 示例:依赖一个远程第三方包
// .package(url: "https://github.com/某用户/某仓库.git", from: "1.0.0"),
],
targets: [
// 业务模块 target,默认源码目录 Sources/MyComponent
.target(
name: "MyComponent",
dependencies: [
// 若依赖上面的第三方包,写成:
// .product(name: "远程库产物名", package: "仓库名")
]
),
// 单元测试 target
.testTarget(
name: "MyComponentTests",
dependencies: ["MyComponent"]
),
],
// Swift 语言版本:兼容旧代码可设 [.v5];不写则用工具版本默认值
swiftLanguageModes: [.v5]
)
示例二:引入 framework(二进制 XCFramework)
适用于:依赖已经打包好的闭源 SDK(.xcframework),或自己交付二进制闭源组件。有两种来源:
2.1 本地 .xcframework 文件引入(随包分发)
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MySDKWrapper",
platforms: [.iOS(.v15)],
products: [
.library(
name: "MySDKWrapper",
targets: ["MySDKWrapper"]
),
],
targets: [
// 引入本地 .xcframework 文件(放在包内 XCFrameworks/ 目录)
.binaryTarget(
name: "MyThirdPartySDK",
path: "XCFrameworks/MyThirdPartySDK.xcframework"
),
// 业务模块依赖这个二进制 framework
.target(
name: "MySDKWrapper",
dependencies: ["MyThirdPartySDK"]
),
]
)
2.2 远程 URL 下载 .xcframework(托管在服务器 / 云端)
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MySDKWrapper",
platforms: [.iOS(.v15)],
products: [
.library(name: "MySDKWrapper", targets: ["MySDKWrapper"]),
],
targets: [
// 从 URL 下载 xcframework 压缩包(需 .zip 格式,解压后含 .xcframework)
.binaryTarget(
name: "MyThirdPartySDK",
url: "https://你的服务器/MyThirdPartySDK.xcframework.zip",
checksum: "xxxxx" // 用 swift package compute-checksum 生成的 SHA256
),
.target(
name: "MySDKWrapper",
dependencies: ["MyThirdPartySDK"]
),
]
)
⚠️ 远程 URL 引入必须提供
checksum(SHA256),生成方式: `bash swift package compute-checksum MyThirdPartySDK.xcframework.zip⚠️ 二进制闭源 SDK:外部无法跳转源码调试,只能看到编译好的接口(.h / SwiftInterface)。
示例三:远程依赖其他 SPM 包
适用于:你的组件需要依赖远程私有/公开 SPM 仓库(如依赖私有网络库、工具库)。
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MyComponent",
platforms: [.iOS(.v15)],
// 声明远程依赖(多个可并列写)
dependencies: [
// 私有远程仓库:SSH 地址
.package(url: "git@<host>:命名空间/网络库.git", from: "1.0.0"),
// 公开远程仓库:HTTPS 地址
.package(url: "https://github.com/某用户/工具库.git", from: "2.3.0"),
],
products: [
.library(
name: "MyComponent",
targets: ["MyComponent"]
),
],
targets: [
.target(
name: "MyComponent",
dependencies: [
// 通过 .product 引用远程依赖的产物
.product(name: "网络库产物名", package: "网络库"),
.product(name: "工具库产物名", package: "工具库"),
]
),
.testTarget(
name: "MyComponentTests",
dependencies: ["MyComponent"]
),
],
swiftLanguageModes: [.v5]
)
关键点:
dependencies里的.package(url:from:):声明远程仓库 + 最低版本。target里的.product(name:package:):把远程依赖的产物链接进当前模块。- 私有仓库地址用 SSH(
git@<host>:命名空间/仓库名.git),公开仓库可用 HTTPS。- 被依赖的远程仓库同样必须在根目录有 Package.swift,且打语义化 tag。
附录 A:静态库 vs 动态库(ObjC 头文件差异)
| 项目 | .static 静态库(推荐) | .dynamic 动态库 |
|---|---|---|
| 公开头来源 | 编译期暴露头路径,头不进库二进制 | 头打包进 Framework 内部,跟随产物 |
| 头生命周期 | 仅编译阶段生效 | 编译 + 运行时 Framework 自带头 |
publicHeadersPath | 导出头给主工程搜索 | 打包进 framework |
| 私有 OC 头隔离 | ✅ 外部不可访问 | ✅ 外部不可访问 |
| iOS 部署限制 | 几乎无限制,私有组件首选 | 需 Embed & Sign,签名坑多 |
| 适用场景 | 绝大多数私有业务组件、OC 混编 SDK | 动态加载、插件化(iOS 少用) |
Swift 调 OC:SPM 静态库模式下无需桥接文件,OC 头放
publicHeadersPath即可被 Swift 使用。
附录 B:二进制闭源 SPM(XCFramework)
targets: [
.binaryTarget(
name: "MyComponent",
path: "./MyComponent.xcframework"
)
]
适合交付闭源 SDK;不支持源码跳转调试。
附录 C:依赖远程包写法
dependencies: [
.package(url: "git@<host>:命名空间/仓库名.git", from: "1.0.0"),
],
targets: [
.target(
name: "MyComponent",
dependencies: [
.product(name: "远程库产物名", package: "仓库名")
]
)
]
附录 D:排查顺序速查表
| 步骤 | 命令 / 操作 | 目的 |
|---|---|---|
| 1 | ssh -T git@<host> | 验证 SSH,失败先修密钥 |
| 2 | git ls-remote --tags ... | 确认远程存在 tag |
| 3 | git clone 后 swift package describe | 校验 Package.swift 合法 + 在根目录 |
| 4 | defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM NO | 关闭内置 SCM |
| 5 | 清缓存 + 删 Package.resolved + 重启 | 清脏数据 |
| 6 | 添加包先用 Branch(main) 测试 | 区分 tag 问题还是包/SCM 问题 |
| 7 | 关联 App Target 到 Link Binary | 解决 No such module |
附录 E:关键要点总结
- 私有 SPM 用 SSH 地址,不用 HTTPS。
- Package.swift 必须放仓库根目录,且声明
.libraryproduct。 - 必须打纯数字语义化 Tag(
1.0.0,不带 v)。 - Xcode 内置 SCM 有 SSH 指纹 bug →
IDEPackageSupportUseBuiltinSCM NO关闭。 - 添加包最后一步务必勾选 App Target,否则
No such module。 - 删除依赖:右侧 Package Dependencies 减号;界面搞不定就删
Package.resolved+ 清缓存。