跳转至正文

将 Capacitor 插件迁移到 Swift Package Manager

为 Capacitor 插件添加 Swift Package Manager 支持:Package.swift 命名规则、CAPBridgedPlugin、Sources 目录结构、资源、Obj-C code 以及 cap2spm。

将 Capacitor 插件从 CocoaPods 迁移到 Swift Package Manager 的示意图

要将 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")

这会带来两个结果:

  1. 您的包通过本地路径从 node_modules引入,而不是从 git URL。您的 Package.swift 必须位于 npm 包根目录,且其引用的所有内容都必须发布到 npm。
  2. 名称由 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 对比.

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。