CocoaPods 只读倒计时 4 个月:我把祖传 iOS 工程迁到 SPM 的真实 checklist
背景与热点解读:那个陪我们十年的紫宝石,终于要退休了
早上八点,我端着咖啡打开 Slack,第一条消息就是 tech lead 发来的链接:CocoaPods 主数据库将在 2026 年 12 月 2 日锁定为只读。我愣了三秒,然后脱口而出一句不太文明的感叹。不是我不想拥抱新事物,而是我手上有三个项目、两个 RN 混合工程、一个从 2015 年传下来的 ObjC 老骨架,它们的血管里流淌的全是 .xcworkspace 和 Pods/ 目录。
但冷静下来想想,这件事其实早有预兆。Firebase 宣布 2026 年 10 月后不再更新 CocoaPods 版本,Google 的 SDK 也在往 SPM 倾斜。CocoaPods 为 iOS 生态做了巨大贡献——它让早期 Swift 和 ObjC 的依赖管理变得可行,让 RN 和 Flutter 能插进原生项目,也让我们在 pod install 的咖啡时间里建立了深厚的同事友谊。可时代确实变了:SPM 从 Xcode 11 一路修到 Xcode 27,终于不再是"玩具";Apple 把新框架、新示例甚至 WWDC demo 都默认写成 Package.swift;而 CocoaPods 的维护精力,肉眼可见地在收缩。
所以这不是"要不要迁"的问题,而是"在 Deadline 前能不能安全迁完"。我的结论是:未来四个月是最后窗口,越往后越被动。与其等到 12 月某天下班前发现某个安全补丁拉不下来,不如现在就开始动手。
技术原理拆解:SPM 不是 Podfile 的语法糖
很多人以为迁移就是把 pod 'Alamofire' 改成 .package(url: ...),这种想法就像以为把手动挡换成自动挡只需要换个档把。SPM 和 CocoaPods 在依赖图、构建模型和原生集成方式上都有本质差异。
CocoaPods 的做法是:读取 Podfile,生成一个庞大的 .xcworkspace,把所有依赖作为独立的 Xcode target 插进项目,编译时大家一起链。它的优点是灵活——你可以写 podspec 里的 prepare_command、script_phase、vendored_frameworks,几乎能做任何 hack;缺点是这种灵活性让项目结构变得很重,Pods/ 目录动辄几个 G,而且任何一步脚本失败都会让 CI 变成抽奖。
SPM 则是声明式的。你在 Package.swift 里描述依赖、target、产品和平台要求,Xcode 负责解析依赖图、下载源码或 binary target、生成构建计划。它的编译模型更贴近 Swift 编译器本身,增量构建和缓存比 CocoaPods 干净很多。但这也意味着:如果你之前的依赖大量依赖 CocoaPods 特有的生命周期钩子或脚本注入,直接迁移会碰壁。
我在拆解时画了三张表:
- 纯 Swift 源码库:迁移成本最低,基本都是直接替换。
- 含 ObjC/C/C++ 的库:需要检查 module map、umbrella header、编译器标志。
- 二进制 xcframework:这是重头戏,SPM 的
.binaryTarget必须配 checksum,而且不支持 CocoaPods 那种动态注入资源脚本的方式。
还有一个容易忽略的点是资源文件。CocoaPods 用 resources 字段可以直接把 bundle 打进 pod,SPM 则需要明确声明 .process() 或 .copy()。我踩的第一个坑就是某个自定义 UI 库的图片没显示,查了半天才发现 bundle 路径从 Bundle(for:) 变成了 Bundle.module。
代码示例或实操演示:把一行 pod 翻译成 Package.swift
光说不练假把式。我举一个真实的迁移片段。以前我们的 Podfile 里有这么一段:
platform :ios, '16.0'
use_frameworks!
target 'MyApp' do
pod 'Alamofire', '~> 5.10'
pod 'Kingfisher', '~> 8.0'
pod 'FirebaseAnalytics', '11.7.0'
end
迁完之后,项目的依赖入口变成 Package.swift:
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MyAppDependencies",
platforms: [.iOS(.v16)],
products: [
.library(
name: "MyAppDependencies",
targets: ["MyAppDependencies"]
),
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.10.0"),
.package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.0.0"),
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "11.7.0"),
],
targets: [
.target(
name: "MyAppDependencies",
dependencies: [
.product(name: "Alamofire", package: "Alamofire"),
.product(name: "Kingfisher", package: "Kingfisher"),
.product(name: "FirebaseAnalytics", package: "firebase-ios-sdk"),
]
),
]
)
然后在 Xcode 里新建一个 Local Swift Package,把它作为 app target 的依赖嵌入。注意 package 参数必须和仓库的 Package name 对上,而不是 URL 里的路径。我因为这个大小写问题浪费了二十分钟——firebase-ios-sdk 的 Package name 是小写,但 product 名是 FirebaseAnalytics,新手很容易在这里打转。
对于那些只发了 CocoaPods、没发 SPM 的私有库,我用了一个过渡方案:本地 git 仓库 + 手写 Package.swift。把库的源码或 xcframework 放到一个内部 Git 仓库里,再用 .package(url: "../LocalVendorLib", branch: "main") 的方式引用。这样不需要等上游迁移,自己先把路铺平。
binary target 的例子也放一下,因为很多人卡在 checksum:
.binaryTarget(
name: "SomeVendorSDK",
url: "https://internal.example.com/SomeVendorSDK.xcframework.zip",
checksum: "a1b2c3d4..."
)
checksum 用 swift package compute-checksum SomeVendorSDK.xcframework.zip 生成。千万别手动抄,也别让 CI 每次现算——我把它写进了下载脚本里,和版本号一起提交。
实践建议:分阶段迁移,别让周五下午变成噩梦
迁移祖传工程最忌讳的就是"一把梭"。我第一周就吃过这个亏:头脑一热删了 Podfile,结果项目里某个 RN bridge 的依赖还没 SPM 化,周一下午全组陪我一起 revert。后来我定了一个四阶段策略,才让整个流程可控。
第一阶段:盘点与冻结。 把当前 Podfile.lock 里所有依赖列成表格,标注每个库是否支持 SPM、支持版本、是否有原生代码或脚本。同时冻结 CocoaPods 版本,不再引入新的 pod。这个表格是迁移的地图,没有它就是在迷雾里开车。
第二阶段:低风险替换。 先把 Alamofire、Kingfisher、SnapKit 这类纯 Swift 库迁走。它们没有脚本、没有资源、没有 ObjC 头文件,能让你快速建立信心,也让 Pods/ 目录瘦一圈。
第三阶段:硬骨头攻坚。 Firebase、Google SDK、RN/Flutter 混合依赖、私有二进制库放在这一步。每迁一个都要跑完整回归:unit test、UI test、archive、App Store 上传。不要嫌麻烦,很多坑在本地 debug 编译时根本不出来,只有在 archive 或 upload 时才会暴露。
第四阶段:清理与 CI 改造。 等 Podfile 里只剩"不得不保留"的项之后,把 CocoaPods 从 CI 里移除,改用 xcodebuild -resolvePackageDependencies 和 SwiftPM 缓存。我们 CI 用的是 GitHub Actions,把 DerivedData/SourcePackages 做了缓存键,构建时间反而比 CocoaPods 快了接近三分之一。
还有一个血泪建议:保留回滚分支。我在迁移前打了一个 before-spm-migration 标签,每个阶段结束也打标签。有一次 Firebase 的 SPM 版本在 archive 时签名异常,我直接切回上一个标签继续干活,避免了周五晚上的集体加班。
总结与展望
CocoaPods 进入只读模式,不代表它明天就炸了,但它意味着我们不能再对旧依赖管理方式心存侥幸。作为一名早上刚喝完咖啡、准备继续和 Xcode 搏斗的 iOS 工程师,我对这件事的情绪是复杂的:一方面确实舍不得那个熟悉的紫色图标和 pod install 的仪式感;另一方面也不得不承认,SPM 的声明式模型、更快的解析速度和更干净的工程结构,才是未来几年 iOS 开发的主航道。
我的迁移还没 100% 完成,但核心链路已经跑通。最大的收获不是学会了写 Package.swift,而是重新梳理了项目的依赖边界。很多老依赖其实根本没用,只是没人敢删;很多 CocoaPods 脚本其实可以被 Xcode Build Phase 或 SwiftPM Plugin 替代。迁移的过程,其实是一次工程体检。
如果你手上也有祖传项目,我的建议很简单:这个月开始盘点,下个月开始低风险替换,年底前完成主体迁移。不要等到 12 月 2 日那天,才发现某个安全补丁只能在 SPM 上拉取。到那时,你面对的就不只是技术问题,而是业务风险了。
至于 Xcode 27 的 Agentic Coding?我当然想试。但我更想先让自己的项目能在 2027 年继续体面地编译通过。先把地基打好,再谈 AI 结对编程,这才是工程师的做事方式。