Let’s migrate a Capacitor iOS plugin so it supports SPM as well as CocoaPods. For why SPM matters and what you gain, see:
https://zenn.dev/rdlabo/articles/4456315c9ca829
1. Rename the Plugin Files
Here, “plugin files” means the header and .m files under ios/Plugin, plus any Swift file marked with @objc that Capacitor loads first as the plugin entry. Older layouts look like this:
- Plugin.swift
- Plugin.m
- Plugin.h
That is confusing, so rename to the newer layout below. ** is the plugin name — for the AdMob plugin, read **Plugin.swift as AdMobPlugin.swift.
- **Plugin.swift
- **Plugin.m
- **Plugin.h
You will delete Plugin.xcodeproj and similar files eventually, so you do not need to rename in Xcode to fix references — changing filenames on disk is enough. Next, open **Plugin.swift. If the type is not named **Plugin, rename it:
- @objc(Stripe)
- public class Plugin: CAPPlugin {
+ @objc(StripePlugin)
+ public class StripePlugin: CAPPlugin {
2. Convert with capacitor-plugin-converter
Use https://github.com/ionic-team/capacitor-plugin-converter to automate what it can. The workflow is simple. First download the zip anywhere:
% curl -OL https://github.com/ionic-team/capacitor-plugin-converter/releases/latest/download/cap2spm.zip
Double-click to get a Unix executable, then run it. Suppose the payment plugin you are migrating lives in ./payment relative to where you downloaded the tool, and the ** part of **Plugin.swift is PaymentPlugin. Run:
% ./cap2spm payment --no-backup
Without --no-backup, existing files are renamed to .old. If you manage the plugin in Git, you usually do not need that. The tool deletes **Plugin.h and **Plugin.m and moves their contents into the pluginMethods property on **Plugin.swift, similar to:
SPM does not load header and .m files, so this migration is required. A Package.swift is also generated at the top level.
3. Create a Fresh Plugin Inside the Repo
I tried several approaches; copying the ios folder into a brand-new plugin scaffold caused the fewest problems. Create one like this:
% cd payment # Assume payment is the plugin directory for this example
% npm init @capacitor/plugin@latest -- --package-id com.hoge.huga --repo https://example.com --author "hoge" --license MIT --description "hoge" ## Create a new plugin to copy into the plugin directory
To avoid extra prompts, I fill optional fields with placeholders in the command. You will still be asked:
✔ What should be the npm package of your plugin?
… Enter the current plugin's npm name
✔ What directory should be used for your plugin?
… Plugin directory name. Use `new-template` here.
✔ What should be the class name for your plugin?
… Enter the value for the "**" part of **Plugin.swift**.
npm install runs after files are generated; you can cancel midway if you like.
4. Refresh the iOS Folder
Next, rebuild the layout: take what you need from new-template and keep what you still need from the old tree. Do not change Web or Android code here — be careful not to delete or overwrite them by mistake.
4.1. Overwrite new-template with Existing Plugin Code
Move existing plugin code (contents of ios/plugins) into new-template/ios/Sources/**Plugin.
4.2. Overwrite the Existing ios Folder with new-template/ios
You can delete the old ios folder. The structure changed a lot from legacy plugins; you could delete obsolete files and fix paths by hand, but for maintainability a clean layout is better.
Delete the old ios folder and put new-template/ios in its place.
4.4. Overwrite the Top-Level Package.swift with new-template/Package.swift
Step 2 also generated a Package.swift, but overwrite it with the one from the new scaffold. In most cases the freshly generated manifest is cleaner than the converter output.
4.5. Overwrite the Top-Level package.json with new-template/package.json
Overwrite package.json as well. The files field for npm publish changed, and older plugins often pin very different devDependency versions. At minimum, review the diff.
4.6 Delete new-template
Delete the new-template directory — you no longer need it.
4.7. Update the **.podspec File
Update the iOS directory path in the podspec:
- s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
5. Update Package.swift
If you have dependencies, update Package.swift. Suppose the podspec had:
s.dependency 'StripePaymentSheet', '~> 23.32.0'
s.dependency 'StripeApplePay', '~> 23.32.0'
You need the same in Package.swift:
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", branch: "main"),
+ .package(url: "https://github.com/stripe/stripe-ios-spm.git", branch: "main")
],
targets: [
.target(
name: "StripePlugin",
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
+ .product(name: "StripePaymentSheet", package: "stripe-ios-spm"),
+ .product(name: "StripeApplePay", package: "stripe-ios-spm")
],
path: "ios/Sources/StripePlugin"),
Because you maintain two package managers, remember to update both podspec and Package.swift when dependencies change.
That finishes the migration!
Summary
I wish capacitor-plugin-converter automated a bit more, but Capacitor plugin layouts evolved over a long time, so some manual steps are inevitable. Copying folders back and forth looks messy, but if you know the plan upfront it is straightforward work — give it a try.
See you next time.