Saltare al contenuto principale

Migra un plugin Capacitor a Swift Package Manager

Aggiungi supporto al gestore di pacchetti Swift a un plugin Capacitor: regole di denominazione Package.swift, CAPBridgedPlugin, layout Sources, risorse, Obj-C code e cap2spm.

Crediti dell'articolo

Martino Donadieu

Autore

Valeria

Recensore

Jordan

Curatore

Migrare un plugin a Capacitor al gestore di pacchetti Swift

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:

  1. La tua package è consumata da un percorso locale da node_modules, non da una URL Git. Il tuo Package.swift deve essere posizionato alla radice del package npm, e tutto ciò che si riferisce deve essere pubblicato su npm.
  2. 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.9 unless 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" Usa capacitor-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: o exact: causerà conflitti di risoluzione.
  • Il nome di destinazione può essere qualsiasi cosa, ma diventa il nome del modulo Swift. Evita nomi generici come Plugin che 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 di CAP_PLUGIN (il nome della classe).
  • jsName è l'argomento di seconda posizione, il nome utilizzato in registerPlugin('MyPlugin') in il tuo TypeScript.
  • Ogni CAP_PLUGIN_METHOD diventa uno CAPPluginMethod, mantenendo lo stesso tipo di ritorno (CAPPluginReturnPromise, CAPPluginReturnCallback o CAPPluginReturnNone).

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/plugin e 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-support abilità in Capgo Abilità passa attraverso Package.swift, pulizia del bridge, risorse e package.jsonInstalla con bunx 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.

Aggiornamenti in tempo reale per le app Capacitor

Quando un bug di layer web è attivo, invia la correzione attraverso Capgo invece di aspettare giorni per l'approvazione della store. Gli utenti ricevono l'aggiornamento in background mentre le modifiche native rimangono nel normale percorso di revisione.

Ultimi articoli dal nostro Blog

Capgo ti offre le migliori informazioni che ti servono per creare un'app mobile davvero professionale.