Pour migrer un plugin Capacitor vers Swift Package Manager, ajoutez un fichier Package.swift à la racine du plugin, dont le nom du package et du produit correspond à ce que Capacitor CLI attend, déplacez le code iOS dans ios/Sources/<Target>, remplacez les fichiers Objective-C CAP_PLUGIN de bridge par CAPBridgedPlugin la conformance en Swift, et ajoutez-le Package.swift à la liste du npm files list. Keep the podspec so CocoaPods apps keep working.
Depuis Capacitor 8, cap add ios crée des projets SPM par défaut. Un plugin sans Package.swift est ignoré dans ces applications avec l’avertissement « Some installed Capacitor plugins are not compatible with SPM », et les utilisateurs voient ensuite « plugin is not implemented » à l’exécution. Ce guide couvre la migration manuelle, l’outil de conversion et les aspects souvent omis : règles de nommage, ressources, code Objective-C et dépendances tierces.
Les équipes qui migrent leur propre projet doivent lire Comment migrer votre application Capacitor vers SPM plutôt.
Comment Capacitor consomme votre package
Activé bunx cap sync ios, les réécritures CLI ios/App/CapApp-SPM/Package.swift dans l’application. Pour chaque plugin Capacitor qui possède un Package.swift, il ajoute deux lignes :
.package(name: "CapgoCapacitorUpdater", path: "../../../node_modules/@capgo/capacitor-updater")
// ...
.product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater")
Deux conséquences :
- Votre package est consommé par chemin local depuis
node_modules, et non depuis une URL git. VotrePackage.swiftdoit se trouver à la racine du package npm, et tout ce qu’il référence doit être publié sur npm. - Le nom est dérivé du nom npm, et non depuis votre podspec. CLI supprime
@, transforme/et-en_, puis convertit chaque segment en camelCase et met la première lettre en majuscule :
| package npm | Nom du package et du produit requis |
|---|---|
@capacitor/haptics |
CapacitorHaptics |
@capgo/capacitor-updater |
CapgoCapacitorUpdater |
capacitor-my-plugin |
CapacitorMyPlugin |
@acme/capacitor-scanner |
AcmeCapacitorScanner |
Si votre Package.swift utilise un autre nom ou un autre nom de produit, l’application échoue à résoudre les packages avec une erreur du type product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.
Étape 1 : écrire Package.swift
C’est la structure utilisée par les plugins officiels. Voici le manifeste réel de @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")
]
)
Notes :
- Conservez
swift-tools-version: 5.9sauf si vous avez besoin d'une fonctionnalité plus récente. Le package généré par l'application utilise également 5,9 par défaut, et Capacitor 8 ne prend pas encore officiellement en charge Swift 6. - Utiliser
from: "8.0.0"pourcapacitor-swift-pm. L'application fixe une version exacte correspondant à celle installée@capacitor/ios, et une plage permet à SPM de résoudre les deux.branch:ouexact:provoquera des conflits de résolution. - Le nom cible peut être n’importe quoi, mais il devient le nom du module Swift. Évitez les noms génériques comme
Pluginqui entrent en collision avec d'autres plugins.
Étape 2 : déplacer les sources
SPM s'attend à un dossier par cible. Le convertisseur et le modèl’officiel utilisent :
my-plugin/
├── Package.swift
├── MyPlugin.podspec
├── ios/
│ ├── Sources/
│ │ └── MyPlugin/
│ │ ├── MyPlugin.swift
│ │ └── MyPluginImplementation.swift
│ └── Tests/
│ └── MyPluginTests/
│ └── MyPluginTests.swift
└── package.json
Supprimez ensuite ce dont SPM n’a pas besoin : ios/Plugin.xcodeproj, ios/Plugin.xcworkspace, ios/Podfile, ios/Plugin/Info.plist et ios/PluginTests/Info.plist.
Mettez à jour le podspec pour que CocoaPods compile les mêmes fichiers :
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'
Étape 3 : remplacer le pont Objective-C par CAPBridgedPlugin
Les anciens plugins enregistrent les méthodes dans Plugin.m:
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(MyPlugin, "MyPlugin",
CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(startScan, CAPPluginReturnCallback);
)
SwiftPM ne peut pas mélanger Objective-C et Swift dans une seule cible, donc cela se déplace dans la classe 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
// ...
}
}
Règles de correspondance :
identifierest le premier argument deCAP_PLUGIN(le nom de la classe).jsNameest le deuxième argument, le nom utilisé dansregisterPlugin('MyPlugin')dans votre TypeScript.- Chaque
CAP_PLUGIN_METHODdevient unCAPPluginMethod, en conservant le même type de retour (CAPPluginReturnPromise,CAPPluginReturnCallbackouCAPPluginReturnNone).
Une méthode absente de pluginMethods compile sans erreur et n'échoue que lorsque JavaScript l'appelle. Cherchez @objc func avec un CAPPluginCall le paramètre et comptez-les par rapport au tableau. Puis supprimez Plugin.h et Plugin.m.
CAPBridgedPlugin fonctionne aussi avec CocoaPods, donc le même fichier Swift sert aux deux gestionnaires de paquets.
Étape 4 : mettre à jour package.json et .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"
}
}
Le -scheme est le nom de votre paquet. Oublier Package.swift ou ios/Sources dans files est la raison la plus courante pour laquelle un plugin fonctionne via un git checkout mais casse après npm publish. Exécutez bun pm pack --dry-run et vérifier la liste des fichiers.
Ajouter la sortie de build SPM à .gitignore:
.build/
Package.resolved
/Packages
.swiftpm/
Étape 5 : gérer ce que le convertisseur ne fait pas
Dépendances tierces
Une ligne podspec comme s.dependency 'Alamofire', '~> 5.9' devient une dépendance de package et un produit sur votre cible :
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")
]
Si le SDK ne fournit qu'un .xcframework, livrez-le dans le package npm et utilisez une cible binaire :
.binaryTarget(name: "VendorSDK", path: "ios/Frameworks/VendorSDK.xcframework")
Si un fournisseur ne propose ni package SPM ni xcframework, vous ne pouvez pas encore ajouter le support SPM. Gardez le plugin en CocoaPods uniquement et précisez-le dans le README.
Gardez les plages de versions dans Package.swift et le podspec aligné. Deux plugins dans la même app qui exigent des versions incompatibles d’un même SDK échoueront à la résolution dans n’importe quel package manager.
Ressources et manifestes de confidentialité
Images, JSON, storyboards et PrivacyInfo.xcprivacy doivent être déclarés :
.target(
name: "MyPlugin",
dependencies: [/* ... */],
path: "ios/Sources/MyPlugin",
resources: [
.process("Resources"),
.copy("PrivacyInfo.xcprivacy")
])
CocoaPods exige que les mêmes fichiers soient déclarés dans le podspec. Placez-les dans un resource bundle nommé :
s.resource_bundles = {
'MyPluginResources' => [
'ios/Sources/MyPlugin/Resources/**/*',
'ios/Sources/MyPlugin/PrivacyInfo.xcprivacy'
]
}
Dans code, les ressources SPM se trouvent dans Bundle.module, alors que CocoaPods les place dans ce MyPluginResources.bundle, à côté de la classe du plugin. Basculez avec le SWIFT_PACKAGE flag, que SwiftPM définit automatiquement :
#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
Du vrai Objective-C code
Si une partie du plugin est en Objective-C (pas seulement le bridge), placez-la dans sa propre cible avec des headers publics dans un include dossier, et faites dépendre la cible Swift de celle-ci :
.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")
Le code Swift effectue ensuite import MyPluginObjC.
La méthode automatisée : cap2spm
de l’équipe Ionic capacitor-plugin-converter génère un cap2spm binaire qui lit Plugin.m et Plugin.h, ajoute la CAPBridgedPlugin conformité à votre classe Swift, génère Package.swift, déplace les fichiers vers Sources et Tests, met à jour le podspec et package.json, et supprime les anciens fichiers de projet 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
Il est conçu pour les plugins exclusivement en Swift, à l’exception des fichiers de bridge. Exécutez-le sur un arbre git propre, puis effectuez l’étape 5 manuellement.
Autres options :
- Générer un nouveau plugin par
bun create @capacitor/pluginet y copier votre implémentation. Le template prend déjà en charge SPM et CocoaPods. C’est souvent plus rapide pour les petits plugins. - Utiliser un agent. Le
capacitor-plugin-spm-supportskill dans skills Capgo open source parcourt Package.swift, le nettoyage du bridge, les ressources etpackage.json. Installez avecbunx skills add Cap-go/capgo-skills.
Tester dans une vraie app 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
Vérifiez que ios/App/CapApp-SPM/Package.swift liste votre package, et que la synchro a affiché « All Capacitor plugins have a Package.swift file ». Compilez ensuite sur un appareil et appelez chaque méthode. Répétez dans une app créée avec bunx cap add ios --packagemanager CocoaPods pour confirmer que le podspec fonctionne toujours.
Troubleshooting
product 'X' required by package 'capapp-spm' ... not found. Le nom du package ou du produit ne correspond pas au nom dérivé de votre nom npm. Consultez le tableau ci-dessus.
"MyPlugin" plugin is not implemented on ios. Soit Package.swift n’a pas été publié, jsName ne correspond pas registerPlugin, ou la classe ne possède pas CAPBridgedPlugin. Inspectez le dossier du plugin dans node_modules.
Multiple targets named 'Plugin' ou des erreurs de duplication de module. Deux plugins utilisent le même nom de target. Renommez le vôtre avec un nom unique. Les équipes peuvent aussi contourner le problème avec experimental.ios.spm.packageOptions (moduleAliases ou symlink) dans la config Capacitor, disponible depuis CLI 8.4.
target 'X' contains mixed language source files. Les fichiers Objective-C et Swift partagent un même dossier. Séparez-les en deux targets.
Xcode shows stale package errors after fixes. Utiliser File > Packages > Reset Package Caches, puis relancez le build.
Si vous avez aussi besoin de fonctionnalités optionnelles activables par app, lisez comment utiliser les traits de package SPM dans Capacitor. Pour les applications qui doivent encore utiliser CocoaPods, consultez comment utiliser CocoaPods avec Capacitor 8, et pour une vue d’ensemble, SPM vs CocoaPods pour Capacitor.