Um ein Capacitor Plugin auf Swift Package Manager zu migrieren, füge eine Package.swift im Plugin-Root hinzu, deren Package- und Produktname mit dem übereinstimmen, was Capacitor CLI erwartet, und verschiebe den iOS code nach ios/Sources/<Target>, ersetze die Objective-C- CAP_PLUGIN Bridge-Dateien durch CAPBridgedPlugin Konformität in Swift und füge Package.swift zum npm hinzu files Liste. Behalte die Podspec bei, damit CocoaPods-Apps weiter funktionieren.
Seit Capacitor 8 cap add ios erstellt standardmäßig SPM-Projekte. Ein Plugin ohne Package.swift wird in diesen Apps mit der Warnung „Some installed Capacitor plugins are not compatible with SPM“ übersprungen, und Nutzer sehen zur Laufzeit „plugin is not implemented“. Dieser Guide behandelt die manuelle Migration, das Converter-Tool und die Punkte, die die meisten Guides auslassen: Naming-Konventionen, Ressourcen, Objective-C code und Drittanbieter-Abhängigkeiten.
App-Teams, die ihr eigenes Projekt migrieren, sollten lesen: So migrierst du deine Capacitor-App auf SPM stattdessen.
Wie Capacitor dein Paket konsumiert
An bunx cap sync ios, schreibt die CLI um ios/App/CapApp-SPM/Package.swift in der App. Für jedes Capacitor Plugin, das einen Package.swift, fügt es zwei Zeilen hinzu:
.package(name: "CapgoCapacitorUpdater", path: "../../../node_modules/@capgo/capacitor-updater")
// ...
.product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater")
Zwei Konsequenzen:
- Dein Package wird über einen lokalen Pfad von
node_modules, nicht über eine Git-URL bezogen. DeinPackage.swiftmuss im npm Package-Root liegen, und alles, worauf es verweist, muss auf npm veröffentlicht sein. - Der Name leitet sich vom npm-Namen ab, nicht von deinem Podspec. Die CLI entfernt
@, wandelt/und-in_, wobei jedes Segment in camelCase umgewandelt und der erste Buchstabe großgeschrieben wird:
| npm Package | Erforderlicher Package- und Produktname |
|---|---|
@capacitor/haptics |
CapacitorHaptics |
@capgo/capacitor-updater |
CapgoCapacitorUpdater |
capacitor-my-plugin |
CapacitorMyPlugin |
@acme/capacitor-scanner |
AcmeCapacitorScanner |
Falls dein Package.swift einen anderen Namen oder Produktnamen verwendet, schlägt das Auflösen der Packages mit einem Fehler wie diesem fehl: product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.
Schritt 1: Package.swift schreiben
Dies ist das Layout, das die offiziellen Plugins verwenden. Hier ist das echte Manifest von @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")
]
)
Hinweise:
- Behalten
swift-tools-version: 5.9außer du benötigst ein neueres Feature. Das generierte Package der App nutzt standardmäßig ebenfalls 5.9, und Capacitor 8 unterstützt Swift 6 noch nicht offiziell. - Nutzt
from: "8.0.0"fürcapacitor-swift-pm. Die App pinnt eine exakte Version, die mit der installierten übereinstimmt@capacitor/ios, und ein Bereich lässt SPM beide auflösen.branch:oderexact:verursacht Auflösungskonflikte. - Der Zielname kann beliebig sein, wird aber zum Swift-Modulnamen. Vermeide generische Namen wie
Plugindie mit anderen Plugins kollidieren.
Schritt 2: Quellen verschieben
SPM erwartet einen Ordner pro Target. Der Konverter und das offizielle Template verwenden:
my-plugin/
├── Package.swift
├── MyPlugin.podspec
├── ios/
│ ├── Sources/
│ │ └── MyPlugin/
│ │ ├── MyPlugin.swift
│ │ └── MyPluginImplementation.swift
│ └── Tests/
│ └── MyPluginTests/
│ └── MyPluginTests.swift
└── package.json
Entferne dann alles, was SPM nicht benötigt: ios/Plugin.xcodeproj, ios/Plugin.xcworkspace, ios/Podfile, ios/Plugin/Info.plist und ios/PluginTests/Info.plist.
Aktualisiere die podspec, damit CocoaPods dieselben Dateien kompiliert:
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'
Schritt 3: Objective-C-Brücke durch CAPBridgedPlugin ersetzen
Ältere Plugins registrieren Methoden in Plugin.m:
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(MyPlugin, "MyPlugin",
CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(startScan, CAPPluginReturnCallback);
)
SwiftPM kann Objective-C und Swift nicht in einem Target mischen, daher wird dies in die Swift-Klasse verschoben:
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
// ...
}
}
Zuordnungsregeln:
identifierist das erste Argument vonCAP_PLUGIN(der Klassenname).jsNameist das zweite Argument, der Name, der inregisterPlugin('MyPlugin')in deinem TypeScript verwendet wird.- Jedes
CAP_PLUGIN_METHODwird zu einemCAPPluginMethod, mit demselben Rückgabetyp (CAPPluginReturnPromise,CAPPluginReturnCallbackoderCAPPluginReturnNone).
Eine Methode, die in pluginMethods fehlt, kompiliert problemlos und schlägt erst fehl, wenn JavaScript sie aufruft. Grep nach @objc func mit einem CAPPluginCall Parameter und vergleiche sie mit dem Array. Dann lösche Plugin.h und Plugin.m.
CAPBridgedPlugin funktioniert auch mit CocoaPods, sodass dieselbe Swift-Datei für beide Package Manager verwendet werden kann.
Schritt 4: package.json und .gitignore aktualisieren
{
"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"
}
}
Der -scheme value ist dein Package-Name. Das zu vergessen Package.swift oder ios/Sources in einem files ist der häufigste Grund, warum ein Plugin aus einem Git-Checkout funktioniert und danach kaputtgeht npm publish. Ausführen bun pm pack --dry-run und die Dateiliste prüfen.
Füge den SPM-Build-Output zu .gitignore:
.build/
Package.resolved
/Packages
.swiftpm/
Schritt 5: Dinge regeln, die der Konverter nicht schafft
Third-Party-Abhängigkeiten
Eine podspec-Zeile wie s.dependency 'Alamofire', '~> 5.9' wird zu einer Package-Dependency und einem Product in deinem 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")
]
Liefert SDK nur ein .xcframework, bündle es im npm Package und nutze ein Binary Target:
.binaryTarget(name: "VendorSDK", path: "ios/Frameworks/VendorSDK.xcframework")
Bietet ein Vendor weder ein SPM-Package noch ein xcframework an, kannst du noch keine SPM-Unterstützung hinzufügen. Behalte das Plugin als CocoaPods-only bei und vermerke dies im README.
Halte Versionsbereiche in Package.swift und der podspec synchron. Zwei Plugins in derselben App, die inkompatible Versionen desselben SDK benötigen, führen in beiden Package Managern zu Fehlern bei der Auflösung.
Ressourcen und Privacy-Manifests
Bilder, JSON, Storyboards und PrivacyInfo.xcprivacy müssen deklariert werden:
.target(
name: "MyPlugin",
dependencies: [/* ... */],
path: "ios/Sources/MyPlugin",
resources: [
.process("Resources"),
.copy("PrivacyInfo.xcprivacy")
])
CocoaPods benötigt dieselben Dateien, deklariert in der Podspec. Legen Sie sie in ein benanntes Resource Bundle:
s.resource_bundles = {
'MyPluginResources' => [
'ios/Sources/MyPlugin/Resources/**/*',
'ios/Sources/MyPlugin/PrivacyInfo.xcprivacy'
]
}
In code liegen SPM-Ressourcen in Bundle.module, während CocoaPods sie in das MyPluginResources.bundlelegt, neben der Plugin-Klasse. Umschalten mit dem SWIFT_PACKAGE -Flag, das SwiftPM automatisch definiert:
#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
Echtes Objective-C code
Wenn ein Teil des Plugins Objective-C ist (nicht nur die Bridge), lege es in ein eigenes Target mit öffentlichen Headern in einem include -Ordner ab und lass das Swift-Target davon abhängen:
.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")
Das Swift code macht dann import MyPluginObjC.
Der automatisierte Weg: cap2spm
Des Ionic-Teams capacitor-Plugin-Converter erstellt eine cap2spm Binärdatei, die liest Plugin.m und Plugin.h, fügt CAPBridgedPlugin Konformität zu deiner Swift-Klasse hinzu, generiert Package.swift, verschiebt Dateien nach Sources und Tests, aktualisiert das podspec und package.json, und entfernt die alten Xcode-Projektdateien.
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
Es ist für Plugins konzipiert, die ausschließlich Swift enthalten, abgesehen von den Bridge-Dateien. Führe es auf einem sauberen Git-Tree aus und erledige Schritt 5 manuell.
Weitere Optionen:
- Neues Plugin scaffolden mit
bun create @capacitor/pluginund kopiere deine Implementierung hinein. Die Vorlage unterstützt bereits SPM und CocoaPods. Das ist oft schneller für kleine Plugins. - Nutze einen Agenten. Der
capacitor-plugin-spm-supportSkill in Capgo Skills erklärt Package.swift, Bridge-Cleanup, Ressourcen undpackage.json. Installieren mitbunx skills add Cap-go/capgo-skills.
Teste in einer echten SPM-App
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
Prüft, ob ios/App/CapApp-SPM/Package.swift listet dein Paket auf, und der Sync hat „All Capacitor plugins have a Package.swift file“ ausgegeben. Dann auf einem Gerät bauen und jede Methode aufrufen. Wiederhole das in einer App, die mit bunx cap add ios --packagemanager CocoaPods to confirm the podspec still works.
Fehlerbehebung
product 'X' required by package 'capapp-spm' ... not found. Der Paket- oder Produktname stimmt nicht mit dem aus Ihrem npm-Namen abgeleiteten Namen überein. Siehe die Tabelle oben.
"MyPlugin" plugin is not implemented on ios. Entweder Package.swift wurde nicht veröffentlicht, jsName stimmt nicht überein registerPlugin, oder der Klasse fehlt CAPBridgedPlugin. Prüfen Sie den Plugin-Ordner in node_modules.
Multiple targets named 'Plugin' oder doppelte Modulfehler. Zwei Plugins verwenden denselben Zielnamen. Benennen Sie Ihres in einen eindeutigen Namen um. App-Teams können das auch umgehen mit experimental.ios.spm.packageOptions (moduleAliases oder symlink) in der Capacitor Config, verfügbar seit CLI 8.4.
target 'X' contains mixed language source files. Objective-C- und Swift-Dateien liegen im selben Ordner. Teile sie in zwei Targets auf.
Xcode zeigt nach Korrekturen veraltete Package-Fehler an. Nutzt File > Packages > Reset Package Caches, und führe den Build erneut aus.
Falls du optionale Features pro App umschalten möchtest, lies wie du SPM-Package-Traits in Capacitor nutzt. Für Apps, die vorerst bei CocoaPods bleiben müssen, siehe die Nutzung von CocoaPods mit Capacitor 8, und für den größeren Überblick, SPM vs. CocoaPods für Capacitor.