要将 Capacitor 插件迁移到 Swift Package Manager,请添加一个 Package.swift 位于插件根目录、package 和 product 名称与 Capacitor CLI 期望一致的文件,再将 iOS code 移入其中 ios/Sources/<Target>,将 Objective-C CAP_PLUGIN 桥接文件替换为 CAPBridgedPlugin Swift 中的协议实现,并添加 Package.swift 到 npm files 列表。保留 podspec,让使用 CocoaPods 的应用继续可用。
自 Capacitor 8 版本起, cap add ios 默认创建 SPM 项目。没有 Package.swift 的插件在这些应用中会被跳过,并显示警告“Some installed Capacitor plugins are not compatible with SPM”,用户随后在运行时会看到“plugin is not implemented”。本指南涵盖手动迁移、转换工具,以及大多数指南会跳过的部分:命名规则、资源、Objective-C code 和第三方依赖。
正在迁移自己项目的 App 团队应阅读 如何将您的 Capacitor 应用迁移到 SPM 请改用这种方式。
Capacitor 如何使用你的 package
当 bunx cap sync ios,CLI 会重写 ios/App/CapApp-SPM/Package.swift 在应用中。对于每个 Capacitor 插件,如果其 Package.swift,只需添加两行:
.package(name: "CapgoCapacitorUpdater", path: "../../../node_modules/@capgo/capacitor-updater")
// ...
.product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater")
这会带来两个结果:
- 您的包通过本地路径从
node_modules引入,而不是从 git URL。您的Package.swift必须位于 npm 包根目录,且其引用的所有内容都必须发布到 npm。 - 名称由 npm 名称派生,而非来自你的 podspec。CLI 会剥离
@,将/以及-为_,然后将每个分段转换为 camelCase,并将首字母大写:
| npm 包 | 必需的 package 和 product 名称 |
|---|---|
@capacitor/haptics |
CapacitorHaptics |
@capgo/capacitor-updater |
CapgoCapacitorUpdater |
capacitor-my-plugin |
CapacitorMyPlugin |
@acme/capacitor-scanner |
AcmeCapacitorScanner |
如果你的 Package.swift 使用了其他名称或 product 名称,应用将无法解析 package,并报错如下: product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.
第 1 步:编写 Package.swift
这是官方插件使用的布局。以下是真实的 manifest,来自 @capacitor/haptics 8:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "CapacitorHaptics",
platforms: [.iOS(.v15)],
products: [
.library(
name: "CapacitorHaptics",
targets: ["HapticsPlugin"])
],
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
],
targets: [
.target(
name: "HapticsPlugin",
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm")
],
path: "ios/Sources/HapticsPlugin"),
.testTarget(
name: "HapticsPluginTests",
dependencies: ["HapticsPlugin"],
path: "ios/Tests/HapticsPluginTests")
]
)
注意:
- 保留
swift-tools-version: 5.9,除非你需要更新的功能。应用生成的 package 默认也使用 5.9,且 Capacitor 8 尚未正式支持 Swift 6。 - 使用
from: "8.0.0"来实现capacitor-swift-pm。应用会锁定与其已安装版本完全匹配的确切版本@capacitor/ios,而版本范围则允许 SPM 同时解析两者。branch:或exact:会导致解析冲突。 - 目标名称可以是任意值,但它会成为 Swift 模块名。避免使用像
Plugin这样容易与其他插件冲突的通用名称。
步骤 2:移动源码
SPM 要求每个目标对应一个文件夹。转换工具和官方模板使用:
my-plugin/
├── Package.swift
├── MyPlugin.podspec
├── ios/
│ ├── Sources/
│ │ └── MyPlugin/
│ │ ├── MyPlugin.swift
│ │ └── MyPluginImplementation.swift
│ └── Tests/
│ └── MyPluginTests/
│ └── MyPluginTests.swift
└── package.json
然后删除 SPM 不需要的部分: ios/Plugin.xcodeproj, ios/Plugin.xcworkspace, ios/Podfile, ios/Plugin/Info.plist 以及 ios/PluginTests/Info.plist.
更新 podspec,以便 CocoaPods 编译相同的文件:
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
s.ios.deployment_target = '15.0'
s.dependency 'Capacitor'
s.swift_version = '5.1'
步骤 3:用 CAPBridgedPlugin 替换 Objective-C bridge
旧插件在 Plugin.m:
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(MyPlugin, "MyPlugin",
CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(startScan, CAPPluginReturnCallback);
)
SwiftPM 无法在同一个 target 中混合 Objective-C 和 Swift,因此这部分移到了 Swift 类中:
import Foundation
import Capacitor
@objc(MyPlugin)
public class MyPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "MyPlugin"
public let jsName = "MyPlugin"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "startScan", returnType: CAPPluginReturnCallback)
]
@objc func echo(_ call: CAPPluginCall) {
call.resolve(["value": call.getString("value") ?? ""])
}
@objc func startScan(_ call: CAPPluginCall) {
call.keepAlive = true
// ...
}
}
映射规则:
identifier是CAP_PLUGIN(类名)的第一个参数。jsName是第二个参数,即registerPlugin('MyPlugin')中在 TypeScript 里使用的名称。- 每个
CAP_PLUGIN_METHOD变为一个CAPPluginMethod,保持相同的返回类型(CAPPluginReturnPromise,CAPPluginReturnCallback或CAPPluginReturnNone).
中缺失的方法 pluginMethods 编译正常,仅在 JavaScript 调用时失败。使用 Grep 查找 @objc func 凭借 CAPPluginCall 参数,并与数组进行计数比对。然后删除 Plugin.h 以及 Plugin.m.
CAPBridgedPlugin 也兼容 CocoaPods,因此同一个 Swift 文件可同时服务于两种包管理器。
步骤 4:更新 package.json 和 .gitignore
{
"files": [
"android/src/main/",
"android/build.gradle",
"dist/",
"ios/Sources",
"ios/Tests",
"Package.swift",
"MyPlugin.podspec"
],
"scripts": {
"verify:ios": "xcodebuild -scheme CapacitorMyPlugin -destination generic/platform=iOS"
}
}
本指南中介绍的 -scheme 值是你的包名。忘记 Package.swift 或 ios/Sources 中的 files 是插件在 git checkout 中正常但在之后出现问题的最常见原因 npm publish。运行 bun pm pack --dry-run 并检查文件列表。
将 SPM 构建输出添加到 .gitignore:
.build/
Package.resolved
/Packages
.swiftpm/
步骤 5:处理转换器无法处理的内容
第三方依赖
类似这样的 podspec 行 s.dependency 'Alamofire', '~> 5.9' 会变成包依赖项,并在你的 target 上生成一个 product:
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0")
],
targets: [
.target(
name: "MyPlugin",
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "Alamofire", package: "Alamofire")
],
path: "ios/Sources/MyPlugin")
]
如果 SDK 仅发布一个 .xcframework,请将其打包在 npm 包内,并使用二进制 target:
.binaryTarget(name: "VendorSDK", path: "ios/Frameworks/VendorSDK.xcframework")
如果供应商既不提供 SPM 包,也不提供 xcframework,目前就无法添加 SPM 支持。请保持插件仅支持 CocoaPods,并在 README 中明确说明。
保持版本范围在 Package.swift 和 podspec 中一致。如果同一应用中的两个插件对某个 SDK 要求不兼容的版本,无论使用哪种包管理器,依赖解析都会失败。
资源与隐私清单
图片、JSON、storyboard 以及 PrivacyInfo.xcprivacy 必须声明:
.target(
name: "MyPlugin",
dependencies: [/* ... */],
path: "ios/Sources/MyPlugin",
resources: [
.process("Resources"),
.copy("PrivacyInfo.xcprivacy")
])
CocoaPods 需要在 podspec 中声明相同的文件。请将它们放入命名的资源 bundle 中:
s.resource_bundles = {
'MyPluginResources' => [
'ios/Sources/MyPlugin/Resources/**/*',
'ios/Sources/MyPlugin/PrivacyInfo.xcprivacy'
]
}
在 code 中,SPM 资源位于 Bundle.module,而 CocoaPods 则将其放在该 MyPluginResources.bundle中,紧邻插件类。使用 SWIFT_PACKAGE 标志切换,SwiftPM 会自动定义该标志:
#if SWIFT_PACKAGE
let resourceBundle = Bundle.module
#else
let resourceBundle: Bundle = {
let classBundle = Bundle(for: MyPlugin.self)
guard let url = classBundle.url(forResource: "MyPluginResources", withExtension: "bundle"),
let bundle = Bundle(url: url) else {
return classBundle
}
return bundle
}()
#endif
真实的 Objective-C code
如果插件的一部分是 Objective-C(不仅仅是桥接),请将其放入一个独立的 target 中,并在 include 文件夹中放置公共头文件,并让 Swift target 依赖它:
.target(
name: "MyPluginObjC",
path: "ios/Sources/MyPluginObjC",
publicHeadersPath: "include"),
.target(
name: "MyPlugin",
dependencies: [
"MyPluginObjC",
.product(name: "Capacitor", package: "capacitor-swift-pm")
],
path: "ios/Sources/MyPlugin")
然后 Swift code 会执行 import MyPluginObjC.
自动化方案:cap2spm
Ionic 团队的 capacitor-plugin-converter 构建了一个 cap2spm 二进制文件,用于读取 Plugin.m 以及 Plugin.h, adds CAPBridgedPlugin 一致性,生成 Package.swift,并将文件移动到 Sources 以及 Tests,更新 podspec 并 package.json,并移除旧的 Xcode 项目文件。
curl -OL https://github.com/ionic-team/capacitor-plugin-converter/releases/latest/download/cap2spm.zip
unzip cap2spm.zip
xattr -d com.apple.quarantine ./cap2spm # binary is unsigned
./cap2spm --backup /path/to/my-plugin
该工具适用于除 bridge 文件外仅使用 Swift 的插件。请在干净的 git 工作树上运行,然后手动执行第 5 步。
其他方案:
- 搭建全新的插件脚手架 配合
bun create @capacitor/plugin并将你的实现代码复制进去。该模板已支持 SPM 和 CocoaPods,对小型插件来说通常更快。 - 使用 Agent。 本指南中介绍的
capacitor-plugin-spm-support技能 Capgo 技能 逐步讲解 Package.swift、桥接清理、资源以及package.json。使用以下命令安装bunx skills add Cap-go/capgo-skills.
在真实的 SPM 应用中测试
bun create @capacitor/app spm-test
cd spm-test
bun add @capacitor/ios
bun add ../my-plugin
bun run build
bunx cap add ios # SPM is the default in Capacitor 8
bunx cap sync ios
确认 ios/App/CapApp-SPM/Package.swift 列出你的包,且 sync 输出显示“所有 Capacitor 插件都有 Package.swift 文件”。然后在真机上构建并调用每个方法。在通过 创建的应用中重复此操作 bunx cap add ios --packagemanager CocoaPods 以确认 podspec 仍然有效。
故障排除
product 'X' required by package 'capapp-spm' ... not found. 包或产品名称与从你的 npm 名称推导出的名称不匹配。请参阅上表。
"MyPlugin" plugin is not implemented on ios. 要么 Package.swift 未发布, jsName 不匹配 registerPlugin,或者类缺少 CAPBridgedPlugin。在 node_modules.
Multiple targets named 'Plugin' 或模块重复错误。 两个插件使用了相同的 target 名称。请将其重命名为唯一值。应用团队也可以通过 experimental.ios.spm.packageOptions (moduleAliases 或 symlink)在 Capacitor 配置中解决,自 CLI 8.4 起可用。
target 'X' contains mixed language source files. Objective-C 和 Swift 文件位于同一文件夹。请将它们拆分到两个 target 中。
修复后 Xcode 仍显示过期的包错误。 使用 File > Packages > Reset Package Caches,然后重新构建。
如果你还需要按应用切换可选功能,请阅读 如何在 Capacitor 中使用 SPM 包特性。对于目前必须继续使用 CocoaPods 的应用,请参阅 如何在 Capacitor 8 中使用 CocoaPods,以及更宏观的视角, Capacitor 的 SPM 与 CocoaPods 对比.