iOS Swift Package Manager(SPM)私有组件完整实操

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)

  1. Xcode → Project → Package Dependencies → + → Add Local(添加本地包)
  2. 选中 MyComponent 文件夹
  3. 在需要使用的 Target → Build Phases → Link Binary With Libraries → + 添加该库
  4. 代码 import MyComponent

✅ 优点:无需 Git,修改组件源码主工程实时生效,开发调试首选。 ⚠️ 缺点:只在本机有效,团队共享需每人配本地路径。


第三部分:远程使用(推送私有 Git + Xcode 引入)

3.1 前置准备:SSH 密钥(最重要,90% 失败在这)

bash 复制代码
cat ~/.ssh/id_ed25519.pub      # 查看;No such file 则先生成
ssh-keygen -t ed25519 -C "你的托管平台登录邮箱"   # 一路回车;passphrase 建议留空
  1. 公钥加到 Git 托管账号:设置 → 安全设置 → SSH 公钥

  2. 私钥加载进 ssh-agent:

    bash 复制代码
    ssh-add --apple-use-keychain ~/.ssh/id_ed25519
  3. 测试连通:

    bash 复制代码
    ssh -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 标签 → 底部 +

弹窗内操作:

  1. 左侧保持 Recently Used (不要选中 Collections)
  2. 右上角输入框粘贴 SSH 地址 :git@<host>:命名空间/仓库名.git ❗ 不要 HTTPS(需 PAT 令牌,会过期)
  3. 回车,版本规则:
    • 生产:Version - Up to Next Major Version + 1.0.0
    • 临时调试:Branch → main(绕开 tag 逻辑,便于定位问题)
  4. 勾选要链接的 App Target(务必勾选!否则 No such module)
  5. 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 大小写不一致。
  • 解决顺序 :
    1. 确认仓库根目录 存在 Package.swift(git clone 后 swift package describe 校验);

    2. 确认该 tag 对应 commit 根目录有 Package.swift(旧 tag 常见问题);

    3. 关闭 Xcode 内置 SCM:

      bash 复制代码
      defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM NO
    4. 清理缓存 + 重启 + 删失败依赖 + 重加(先用 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 后):

    bash 复制代码
    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 复制代码
// 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:关键要点总结

  1. 私有 SPM 用 SSH 地址,不用 HTTPS。
  2. Package.swift 必须放仓库根目录 ,且声明 .library product。
  3. 必须打纯数字语义化 Tag (1.0.0,不带 v)。
  4. Xcode 内置 SCM 有 SSH 指纹 bug → IDEPackageSupportUseBuiltinSCM NO 关闭。
  5. 添加包最后一步务必勾选 App Target ,否则 No such module。
  6. 删除依赖:右侧 Package Dependencies 减号;界面搞不定就删 Package.resolved + 清缓存。
相关推荐
随遇丿而安1 小时前
第 4 周:28KB 的 PNG 占了 8MB 内存,因为图片内存看尺寸不看文件大小
ios
leizhen198408092 小时前
Vibe Coding:门槛降了,但判断力成了新的门槛
人工智能·安全·ios
我就是不信7 小时前
Swift 方法派发机制详解
开发语言·ios·swift
AIsoft_86899 小时前
科会通录音工具倍速回放怎么切换速度
ios·智能手机
老李IT笔记10 小时前
设备显示在线但指令没回执:三类静默失效的排查顺序
ios·iphone
芯械研究所10 小时前
她宁、益博思、倍思、绿联哪款ipad电容笔好?四款电容笔款式实测解读
ios·ipad·电容笔·电容笔推荐·电容笔测评
Yannik1 天前
iOS CocoaPods私有库完整搭建+踩坑实战总结
ios·cocoapods
终端安全笔记1 天前
绕过工具把 iOS 27 列进支持范围:从它的更新节奏能读出什么
安全·ios