Per migrare un plugin Capacitor a Swift Package Manager, aggiungi un Package.swift nella root del plugin, dove il nome del package e del prodotto coincide con quanto atteso dal Capacitor CLI, sposta l'code iOS in ios/Sources/<Target>, sostituisci i file Objective-C CAP_PLUGIN file bridge con CAPBridgedPlugin la conformità in Swift e aggiungi Package.swift a npm nella files list. Keep the podspec so CocoaPods apps keep working.
Dalla versione Capacitor 8, cap add ios crea progetti SPM di default. Un plugin senza Package.swift viene saltato in quelle app con l'avviso “Some installed Capacitor plugins are not compatible with SPM”, e gli utenti vedranno “plugin is not implemented” a runtime. Questa guida copre la migrazione manuale, lo strumento di conversione e i dettagli che spesso mancano nelle guide: regole di naming, risorse, code Objective-C e dipendenze di terze parti.
I team che migrano il proprio progetto dovrebbero leggere Come migrare la tua app Capacitor a SPM invece.
Come Capacitor utilizza il tuo package
Su bunx cap sync ios, CLI riscrive ios/App/CapApp-SPM/Package.swift nell'app. Per ogni plugin Capacitor che ha un Package.swift, aggiunge due righe:
.package(name: "CapgoCapacitorUpdater", path: "../../../node_modules/@capgo/capacitor-updater")
// ...
.product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater")
Due conseguenze:
- Il pacchetto viene consumato tramite percorso locale da
node_modules, non da un URL git. Il tuoPackage.swiftdeve trovarsi nella root del pacchetto npm, e tutto ciò che richiama deve essere pubblicato su npm. - Il nome deriva dal nome npm, non dal tuo podspec. Il CLI rimuove
@, trasforma/e-all'interno_, poi applica il camelCase a ogni segmento e la maiuscola alla prima lettera:
| package npm | Nome del package e del prodotto richiesti |
|---|---|
@capacitor/haptics |
CapacitorHaptics |
@capgo/capacitor-updater |
CapgoCapacitorUpdater |
capacitor-my-plugin |
CapacitorMyPlugin |
@acme/capacitor-scanner |
AcmeCapacitorScanner |
Se la tua Package.swift usa un altro nome o nome prodotto, l'app non riesce a risolvere i package con un errore tipo product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.
Passaggio 1: scrivi Package.swift
Questo è il layout usato dai plugin ufficiali. Ecco il manifest reale di @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")
]
)
Note:
- Mantieni
swift-tools-version: 5.9a meno che non ti serva una feature più recente. Anche il package generato dall'app usa 5.9 di default, e Capacitor 8 non supporta ancora ufficialmente Swift 6. - Usa
from: "8.0.0"percapacitor-swift-pm. L'app blocca una versione esatta che corrisponde a quella installata@capacitor/ios, and a range lets SPM resolve both.branch:oexact:causerà conflitti di risoluzione. - Il nome del target può essere qualsiasi cosa, ma diventerà il nome del modulo Swift. Evita nomi generici come
Pluginche vadano in conflitto con altri plugin.
Passaggio 2: sposta le sorgenti
SPM richiede una cartella per ogni target. Il converter e il template ufficiale usano:
my-plugin/
├── Package.swift
├── MyPlugin.podspec
├── ios/
│ ├── Sources/
│ │ └── MyPlugin/
│ │ ├── MyPlugin.swift
│ │ └── MyPluginImplementation.swift
│ └── Tests/
│ └── MyPluginTests/
│ └── MyPluginTests.swift
└── package.json
Poi rimuovi ciò che non serve a SPM: ios/Plugin.xcodeproj, ios/Plugin.xcworkspace, ios/Podfile, ios/Plugin/Info.plist e ios/PluginTests/Info.plist.
Aggiorna il podspec in modo che CocoaPods compili gli stessi file:
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'
Passaggio 3: sostituisci il bridge Objective-C con CAPBridgedPlugin
I vecchi plugin registrano i metodi in Plugin.m:
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(MyPlugin, "MyPlugin",
CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(startScan, CAPPluginReturnCallback);
)
SwiftPM non può mixare Objective-C e Swift in un unico target, quindi questo viene spostato nella 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
// ...
}
}
Regole di mappatura:
identifierè il primo argomento diCAP_PLUGIN(il nome della classe).jsNameè il secondo argomento, il nome usato inregisterPlugin('MyPlugin')nel tuo TypeScript.- Ogni
CAP_PLUGIN_METHODdiventa unCAPPluginMethod, mantenendo lo stesso tipo di ritorno (CAPPluginReturnPromise,CAPPluginReturnCallbackoCAPPluginReturnNone).
Un metodo mancante in pluginMethods compila correttamente e fallisce solo quando JavaScript lo chiama. Cerca @objc func con un CAPPluginCall il parametro e confrontali con l'array. Poi elimina Plugin.h e Plugin.m.
CAPBridgedPlugin funziona anche con CocoaPods, quindi lo stesso file Swift serve entrambi i package manager.
Passaggio 4: aggiorna package.json e .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"
}
}
Il -scheme il valore è il nome del tuo pacchetto. Dimenticare Package.swift o ios/Sources nella files è il motivo più comune per cui un plugin funziona da git checkout e si rompe dopo npm publish. Esegui bun pm pack --dry-run e controlla l'elenco dei file.
Aggiungi l'output di build di SPM a .gitignore:
.build/
Package.resolved
/Packages
.swiftpm/
Passaggio 5: gestire ciò che il convertitore non fa
Dipendenze di terze parti
Una riga podspec come s.dependency 'Alamofire', '~> 5.9' diventa una package dependency e un prodotto sul tuo target:
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")
]
Se SDK distribuisce solo un .xcframework, includilo nel pacchetto npm e usa un binary target:
.binaryTarget(name: "VendorSDK", path: "ios/Frameworks/VendorSDK.xcframework")
Se un vendor non offre un pacchetto SPM né un xcframework, non puoi ancora aggiungere il supporto SPM. Mantieni il plugin solo con CocoaPods e specificalo nel README.
Mantieni i range delle versioni in Package.swift e il podspec allineati. Due plugin nella stessa app che richiedono versioni incompatibili di uno SDK causeranno errori di risoluzione in qualsiasi package manager.
Risorse e manifest privacy
Immagini, JSON, storyboard e PrivacyInfo.xcprivacy devono essere dichiarati:
.target(
name: "MyPlugin",
dependencies: [/* ... */],
path: "ios/Sources/MyPlugin",
resources: [
.process("Resources"),
.copy("PrivacyInfo.xcprivacy")
])
CocoaPods richiede gli stessi file dichiarati nel podspec. Mettili in un resource bundle denominato:
s.resource_bundles = {
'MyPluginResources' => [
'ios/Sources/MyPlugin/Resources/**/*',
'ios/Sources/MyPlugin/PrivacyInfo.xcprivacy'
]
}
In code, le risorse SPM si trovano in Bundle.module, mentre CocoaPods le mette in quella MyPluginResources.bundle, accanto alla classe del plugin. Cambia con il SWIFT_PACKAGE flag, che SwiftPM definisce automaticamente:
#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
Un vero code Objective-C
Se parte del plugin è in Objective-C (non solo il bridge), inseriscila in un target dedicato con header pubblici in una include cartella, e fai dipendere il target Swift da esso:
.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")
Il code Swift esegue quindi import MyPluginObjC.
Il percorso automatizzato: cap2spm
Il team di Ionic capacitor-plugin-converter crea un cap2spm binario che legge Plugin.m e Plugin.h, aggiunge la CAPBridgedPlugin conformance alla tua classe Swift, genera Package.swift, sposta i file in Sources e Tests, aggiorna il podspec e package.json, e rimuove i vecchi file del progetto 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
È pensato per i plugin scritti solo in Swift, fatta eccezione per i file di bridge. Eseguilo su un tree git pulito e poi procedi manualmente al Passaggio 5.
Altre opzioni:
- Genera un nuovo plugin con
bun create @capacitor/plugine incolla la tua implementazione. Il template supporta già SPM e CocoaPods. Spesso è più veloce per i plugin piccoli. - Usa un agente. Il
capacitor-plugin-spm-supportcompetenza in Competenze Capgo guida attraverso Package.swift, la pulizia dei bridge, le risorse epackage.json. Installa conbunx skills add Cap-go/capgo-skills.
Test in un'app SPM reale
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
Verifica che ios/App/CapApp-SPM/Package.swift elenca il tuo package, e quel sync ha stampato “All Capacitor plugins have a Package.swift file”. Poi fai il build su un dispositivo e chiama ogni metodo. Ripeti in un'app creata con bunx cap add ios --packagemanager CocoaPods per confermare che il podspec funzioni ancora.
Risoluzione problemi
product 'X' required by package 'capapp-spm' ... not found. Il nome del Package o del prodotto non corrisponde al nome derivato dal tuo nome npm. Vedi la tabella sopra.
"MyPlugin" plugin is not implemented on ios. O Package.swift non è stato pubblicato, jsName non corrisponde registerPlugin, o alla classe manca CAPBridgedPlugin. Controlla la cartella del plugin in node_modules.
Multiple targets named 'Plugin' o errori di modulo duplicato. Due plugin usano lo stesso target name. Rinomina il tuo con un nome univoco. I team delle app possono aggirarlo anche con experimental.ios.spm.packageOptions (moduleAliases o symlink) nella configurazione Capacitor, disponibile da CLI 8.4.
target 'X' contains mixed language source files. I file Objective-C e Swift condividono una cartella. Dividili in due target.
Xcode mostra errori di pacchetto obsoleti dopo le correzioni. Usa File > Package > Reset Package Caches, quindi esegui di nuovo il build.
Se hai bisogno di attivare feature opzionali per ogni app, leggi come usare i package traits di SPM in Capacitor. Per le app che devono restare su CocoaPods per ora, vedi come usare CocoaPods con Capacitor 8, e per una visione d'insieme, SPM vs CocoaPods per Capacitor.