Swift Package Manager(SPM)私有组件完整实操
适用环境:Xcode 27(内置 Swift 6.4)/ macOS / 私有 Git 托管(Gitee / GitLab / 企业自建) 说明:文中
git@<host>:命名空间/仓库名.git、/Users/你的用户名/...、https://github.com/某用户/某仓库.git均为示例占位符,请替换为真实地址与路径。 阅读路径:① Package.swift 是什么/怎么用 → ② 本地如何使用 → ③ 远程如何使用 → ④ 远程使用踩坑汇总 → ⑤ 附录
第〇部分:整体流程图
scss
[创建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 详解
swift
.library(
name: "MyComponent", // 产物名 → 外部 import MyComponent 写的就是它
targets: ["MyComponent"] // 由哪些内部 target 组成这个库(可多个)
)
name:对外暴露的模块名,外部import XXX写的就是这个名字。targets:数组,可放多个 target 合并成一个对外库。type:可选,.static(静态)/.dynamic(动态),不写 SPM 自动推断,iOS 私有组件一般省略即可。
1.4 targets / .target 详解
swift
.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 校验合法性
bash
cd <包根目录>
swift package describe # 快速验证语法;错误会直接提示
swift package dump-package # 输出完整 JSON,看结构是否符合预期
describe只能在本地 Package 目录执行,不支持直接拉远程(见踩坑 6)。
第二部分:创建 SPM 包 & 本地使用
2.1 命令行创建
bash
mkdir MyComponent && cd MyComponent
swift package init --type library # --type library 代码库;executable 可执行程序
2.2 标准目录结构(尽量遵守,少踩坑)
css
MyComponent/
├─ Package.swift
├─ Sources/
│ ├─ MyComponent/ ← 和 target 名一致
│ │ └─ *.swift
│ └─ Helpers/ ← OC target(若混编)
│ ├─ src/ ← .m 实现
│ └─ include/ ← .h 公开头
└─ Tests/
└─ MyComponentTests/
2.3 最小可用 Package.swift(纯 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 的场景)
swift
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% 失败在这)
bash
cat ~/.ssh/id_ed25519.pub # 查看;No such file 则先生成
ssh-keygen -t ed25519 -C "你的托管平台登录邮箱" # 一路回车;passphrase 建议留空
-
公钥加到 Git 托管账号:设置 → 安全设置 → SSH 公钥
-
私钥加载进 ssh-agent:
bashssh-add --apple-use-keychain ~/.ssh/id_ed25519 -
测试连通:
bashssh -T git@<你的host> # 首次输入 yes;成功输出 authenticated
3.2 推送仓库 & 打 Tag
bash
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:
bashdefaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM NO -
清理缓存 + 重启 + 删失败依赖 + 重加(先用 Branch 测试)。
-
坑 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 后):
bashcd /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
// 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
// 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
// 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
// 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)
swift
targets: [
.binaryTarget(
name: "MyComponent",
path: "./MyComponent.xcframework"
)
]
适合交付闭源 SDK;不支持源码跳转调试。
附录 C:依赖远程包写法
swift
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+ 清缓存。