Per migrare un plugin Capacitor al gestore di pacchetti Swift, aggiungi un Package.swift al root del plugin, il cui nome del pacchetto e del prodotto corrisponde a ciò che Capacitor CLI aspetta, sposta l'code iOS in ios/Sources/<Target>Sostituisci l'Objective-C CAP_PLUGIN file con gli CAPBridgedPlugin conformità in Swift, e aggiungi Package.swift alla lista del npm files mantieni il podspec per far funzionare le app di CocoaPods.
Siccome Capacitor 8, cap add ios crea progetti SPM di default. Un plugin senza Package.swift viene ignorato in quelle app con l'avviso “Alcuni plugin Capacitor installati non sono compatibili con SPM”, e gli utenti vedono poi “plugin non implementato” all'esecuzione. Questa guida copre la migrazione manuale, lo strumento di conversione, e le parti che la maggior parte delle guide trascura: le regole di denominazione, le risorse, l'code Objective-C e le dipendenze di terze parti.
App team che sta spostando il proprio progetto Come Migliare la tua app Capacitor a SPM piuttosto.
Come Capacitor consuma il tuo pacchetto
su bunx cap sync iose le CLI rewrites ios/App/CapApp-SPM/Package.swift in l'applicazione. Per ogni Capacitor plugin che ha un Package.swift, aggiunge due righe:
.package(name: "CapgoCapacitorUpdater", path: "../../../node_modules/@capgo/capacitor-updater")
// ...
.product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater")
Due conseguenze:
- La tua package è consumata da un percorso locale da
node_modules, non da una URL Git. Il tuoPackage.swiftdeve essere posizionato alla radice del package npm, e tutto ciò che si riferisce deve essere pubblicato su npm. - Il nome deriva dal nome del npm, non dal tuo podspec. Il CLI elimina
@, trasforma/e-context_poi, quindi capitalizza ogni segmento e mette la prima lettera maiuscola:
| npm pacchetto | Nome pacchetto e prodotto richiesto |
|---|---|
@capacitor/haptics |
CapacitorHaptics |
@capgo/capacitor-updater |
CapgoCapacitorUpdater |
capacitor-my-plugin |
CapacitorMyPlugin |
@acme/capacitor-scanner |
AcmeCapacitorScanner |
Se il tuo Package.swift utilizza un altro nome o nome prodotto, l'app fallisce nell'aggiornare i pacchetti con un errore come product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.
Passo 1: scrivi Package.swift
Questo è il layout che utilizzano i plugin ufficiali. Ecco il manifesto reale da @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:
- Note:
swift-tools-version: 5.9unless you need a newer feature. The app’s generated package also uses 5.9 by default, and Capacitor 8 doesn’t officially support Swift 6 yet. - Use
from: "8.0.0"Usacapacitor-swift-pm. L'applicazione pinna una versione esatta che corrisponde alla sua installazione@capacitor/iose una gamma consente a SPM di risolvere sia entrambi.branch:oexact:causerà conflitti di risoluzione. - Il nome di destinazione può essere qualsiasi cosa, ma diventa il nome del modulo Swift. Evita nomi generici come
Pluginche collidono con altri plugin.
Passo 2: sposta le sorgenti
SPM aspetta un folder per ogni target. Il convertitore e il template ufficiale utilizzano:
my-plugin/
├── Package.swift
├── MyPlugin.podspec
├── ios/
│ ├── Sources/
│ │ └── MyPlugin/
│ │ ├── MyPlugin.swift
│ │ └── MyPluginImplementation.swift
│ └── Tests/
│ └── MyPluginTests/
│ └── MyPluginTests.swift
└── package.json
Poi rimuovi ciò che SPM non necessita: 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 i medesimi 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'
Passo 3: sostituisci il ponte Objective-C con CAPBridgedPlugin
Plugin vecchi registrano 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ò mescolare Objective-C e Swift in un target, quindi questo si sposta 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 mapping:
identifierè l'argomento di prima posizione diCAP_PLUGIN(il nome della classe).jsNameè l'argomento di seconda posizione, il nome utilizzato inregisterPlugin('MyPlugin')in il tuo TypeScript.- Ogni
CAP_PLUGIN_METHODdiventa unoCAPPluginMethod, mantenendo lo stesso tipo di ritorno (CAPPluginReturnPromise,CAPPluginReturnCallbackoCAPPluginReturnNone).
Una funzione mancante da pluginMethods compila correttamente e fallisce solo quando JavaScript lo chiama. Cerca con Grep per @objc func con un CAPPluginCall parametro e conteggiali contro l'array. Poi cancella Plugin.h e Plugin.m.
CAPBridgedPlugin funziona anche con CocoaPods, quindi lo stesso file Swift serve entrambi i gestori di pacchetti.
Passo 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 valore è il nome del tuo pacchetto. Dimenticando Package.swift o ios/Sources in files il motivo più comune per cui un plugin funziona da un controllo Git e si rompe dopo npm publish. Run bun pm pack --dry-run e controlla la lista dei file.
Aggiungi l'output di costruzione di SPM a .gitignore:
.build/
Package.resolved
/Packages
.swiftpm/
Passo 5: gestisci ciò che il convertitore non
Dipendenze di terze parti
Dipendenze di terze parti s.dependency 'Alamofire', '~> 5.9' diventa una dipendenza del pacchetto 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 il SDK fornisce solo un .xcframework, invia il pacchetto all'interno di npm e utilizza un target binario:
.binaryTarget(name: "VendorSDK", path: "ios/Frameworks/VendorSDK.xcframework")
If a vendor offers no SPM package and no xcframework, you can’t add SPM support yet. Keep the plugin CocoaPods-only and say so in the README.
Mantieni intervalli di versione in Package.swift e il podspec allineato. Due plugin nello stesso app che richiedono versioni incompatibili di uno stesso SDK falliranno la risoluzione in entrambi i gestori dei pacchetti.
Risorse e manifesti di 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 i file dichiarati nella podspec. Mettili in un bundle di risorse denominato:
s.resource_bundles = {
'MyPluginResources' => [
'ios/Sources/MyPlugin/Resources/**/*',
'ios/Sources/MyPlugin/PrivacyInfo.xcprivacy'
]
}
In code, i risorse SPM vivono in Bundle.module, mentre CocoaPods li mette in quello MyPluginResources.bundle, accanto alla classe del plugin. Sostituisci con la 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
Reale Objective-C code
Se una parte del plugin è in Objective-C (non solo il ponte), mettila in un proprio target con intestazioni pubbliche in un 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")
Lo Swift code poi fa: import MyPluginObjC.
La strada automatica: cap2spm
The team di Ionic capacitor-plugin-converter costruisce un cap2spm binario che legge Plugin.m e Plugin.h, adds CAPBridgedPlugin conformità alla tua classe Swift, genera Package.swiftconformità alla tua classe Swift, genera Sources e TestsAggiorna il podspec e package.jsone elimina 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
It’s designed for plugins that are Swift-only apart from the bridge files. Run it on a clean git tree and then do Step 5 by hand.
Altre opzioni:
- Scaffolda un plugin fresco con
bun create @capacitor/plugine copia la tua implementazione dentro. Il template supporta già SPM e CocoaPods. Questo è spesso più veloce per i piccoli plugin. - Usa un agente. La
capacitor-plugin-spm-supportabilità in Capgo Abilità passa attraverso Package.swift, pulizia del bridge, risorse epackage.jsonInstalla conbunx skills add Cap-go/capgo-skills.
Testa 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 elenco i tuoi pacchetti e che sincronizza stampa “Tutti i Capacitor plugin hanno un file Package.swift”. Costruisci quindi su un dispositivo e chiama ogni metodo. Ripeti in un'app creata con bunx cap add ios --packagemanager CocoaPods per confermare che il podspec funziona ancora.
Risolvere i problemi
product 'X' required by package 'capapp-spm' ... not found. Il nome del pacchetto o prodotto non corrisponde al nome derivato dal tuo npm nome. Consulta la tabella sopra.
"MyPlugin" plugin is not implemented on ios. Either Package.swift non è stato pubblicato, jsName non è stato pubblicato, registerPluginnon corrisponde CAPBridgedPlugin. Ispeziona il cartello del plugin in node_modules.
Multiple targets named 'Plugin' . Ispeziona la cartella del plugin in Two plugins use the same target name. Rename yours to something unique. App teams can also work around it with experimental.ios.spm.packageOptions (moduleAliases or symlink) nella Capacitor config, disponibile dal 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 > Pacchetti > Reset Cache dei Pacchetti, poi costruisci nuovamente.
Se hai anche bisogno di funzionalità facoltative abilitate per app, leggi come utilizzare le caratteristiche del package SPM in Capacitor. Per le app che devono rimanere su CocoaPods per ora, vedi come utilizzare CocoaPods con Capacitor 8, e per la visione d'insieme, SPM vs CocoaPods per Capacitor.