Zum Hauptinhalt springen

Capacitor Plugin auf Swift Package Manager migrieren

Swift Package Manager-Unterstützung für ein Capacitor Plugin hinzufügen: Namensregeln für Package.swift, CAPBridgedPlugin, Sources-Layout, Ressourcen, Obj-C code und cap2spm.

Illustration zur Migration eines Capacitor-Plugins von CocoaPods auf Swift Package Manager

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:

  1. Dein Package wird über einen lokalen Pfad von node_modules, nicht über eine Git-URL bezogen. Dein Package.swift muss im npm Package-Root liegen, und alles, worauf es verweist, muss auf npm veröffentlicht sein.
  2. 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.9 auß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ür capacitor-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: oder exact: verursacht Auflösungskonflikte.
  • Der Zielname kann beliebig sein, wird aber zum Swift-Modulnamen. Vermeide generische Namen wie Plugin die 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:

  • identifier ist das erste Argument von CAP_PLUGIN (der Klassenname).
  • jsName ist das zweite Argument, der Name, der in registerPlugin('MyPlugin') in deinem TypeScript verwendet wird.
  • Jedes CAP_PLUGIN_METHOD wird zu einem CAPPluginMethod, mit demselben Rückgabetyp (CAPPluginReturnPromise, CAPPluginReturnCallback oder CAPPluginReturnNone).

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/plugin und 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-support Skill in Capgo Skills erklärt Package.swift, Bridge-Cleanup, Ressourcen und package.json. Installieren mit bunx 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.

Live-Updates für Capacitor-Apps

Wenn ein Bug im Web-Layer live ist, liefere den Fix über Capgo aus, statt Tage auf die App-Store-Freigabe zu warten. Nutzer bekommen das Update im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neues aus unserem Blog

Capgo gibt dir die besten Insights, um eine wirklich professionelle Mobile App zu erstellen.