Saltare al contenuto principale

Migrare un plugin Capacitor verso il gestore di pacchetti Swift

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

Crediti dell'articolo

Martin Donadieu

Autore

Valeria

Recensore

Jordan

Editore

Migrare un Plugin Capacitor al Gestore Pacchetti Swift

Per migrare un plugin Capacitor utilizzando Swift Package Manager, aggiungi un Package.swift Nel root del plugin, il cui nome pacchetto e prodotto corrisponde a ciò che Capacitor CLI aspetta, sposta l'code iOS in ios/Sources/<Target>Sostituisci l'Objective-C CAP_PLUGIN conformità in Swift, e aggiungi CAPBridgedPlugin conformità in Swift, e aggiungi Package.swift al npm files To migrate a __CAPGO_KEEP_0__ plugin to Swift Package Manager, add a

Dal Capacitor 8, cap add ios crea progetti SPM di default. Un plugin senza Package.swift viene ignorato in quegli 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.

Squadre di sviluppo che stanno spostando il proprio progetto How to Migrate Your Capacitor App to SPM instead.

Come Capacitor consuma il tuo pacchetto

On bunx cap sync ios, le CLI ri-scritture ios/App/CapApp-SPM/Package.swift nella tua app. Per ogni plugin Capacitor che ha un Package.swiftDue conseguenze:

.package(name: "CapgoCapacitorUpdater", path: "../../../node_modules/@capgo/capacitor-updater")
// ...
.product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater")

Due a 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 della package npm e tutto ciò che si riferisce deve essere pubblicato su npm.
  2. Il nome è derivato dal nome della package npm, non dal tuo podspec. La CLI elimina @, trasforma / e - into _poi trasforma ogni segmento in camel-case e capitalizza la prima lettera:
La package npm Nome package 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 prodotto, l'app fallisce nella risoluzione dei pacchetti con un errore come product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.

Step 1: scrivi Package.swift

Questo è il layout utilizzato dai 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")
    ]
)

Note:

  • Conserva swift-tools-version: 5.9 se non hai bisogno di una funzionalità più recente. Il pacchetto generato dall'app utilizza 5.9 di default, e Capacitor 8 non supporta ancora Swift 6 ufficialmente.
  • Utilizza from: "8.0.0" per capacitor-swift-pm. L'app pina una versione esatta che corrisponde alla sua installazione @capacitor/ios, and a range lets SPM resolve both. branch: o exact: causerà conflitti di risoluzione.
  • The nome di destinazione può essere qualsiasi cosa, ma diventa il nome del modulo Swift. Evita i nomi generici come Plugin Step 2: sposta le fonti

Passo 2: sposta le risorse

SPM richiede 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

Rimuovi poi ciò che SPM non richiede. ios/Plugin.xcodeproj, ios/Plugin.xcworkspace, ios/Podfile, ios/Plugin/Info.plist and ios/PluginTests/Info.plist.

Step 3: sostituisci il ponte Objective-C con CAPBridgedPlugin

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'

Le vecchie estensioni registrano metodi in

Metodi vecchi plugin registrano 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
        // ...
    }
}

è l'argomento principale

  • identifier è l'argomento principale CAP_PLUGIN (il nome della classe).
  • jsName è il secondo argomento, il nome utilizzato in registerPlugin('MyPlugin') in TypeScript.
  • Ogni CAP_PLUGIN_METHOD diventa uno CAPPluginMethod, mantenendo lo stesso tipo di ritorno (CAPPluginReturnPromise, CAPPluginReturnCallback o CAPPluginReturnNone).

Un metodo mancante da pluginMethods compila correttamente e fallisce solo quando JavaScript lo chiama. Cerca con @objc func con un CAPPluginCall parametro e conteggiali contro l'array. Poi elimina Plugin.h e Plugin.m.

CAPBridgedPlugin Funziona anche con CocoaPods, quindi lo stesso file Swift serve entrambi i gestori dei 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 Il valore è il nome del tuo pacchetto. Dimenticare 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 publishEsegui bun pm pack --dry-run e controlla l'elenco dei file.

Aggiungi output di costruzione SPM .gitignore:

.build/
Package.resolved
/Packages
.swiftpm/

Passo 5: gestisci ciò che il convertitore non gestisce

Dipendenze di terze parti

A podspec line come: s.dependency 'Alamofire', '~> 5.9' diventa una dipendenza di 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 .xcframeworkinvialo all'interno del pacchetto npm e utilizza un target binario:

.binaryTarget(name: "VendorSDK", path: "ios/Frameworks/VendorSDK.xcframework")

Se un fornitore non offre alcun pacchetto SPM e nessuna xcframework, non puoi aggiungere il supporto SPM ancora. Mantieni il plugin solo per CocoaPods e specificalo nel README.

Mantieni le fasce di versione in Package.swift e il podspec allineato. Due plugin nella stessa app che richiedono versioni incompatibili di uno SDK falliranno la risoluzione in entrambi i gestori di 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 identici dichiarati nel podspec. Mettili in un bundle di risorse denominato:

s.resource_bundles = {
  'MyPluginResources' => [
    'ios/Sources/MyPlugin/Resources/**/*',
    'ios/Sources/MyPlugin/PrivacyInfo.xcprivacy'
  ]
}

In code, le risorse SPM vivono in Bundle.module, mentre CocoaPods li mette in quel MyPluginResources.bundle, accanto alla classe del plugin. Sostituisci 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

Real 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 folder, e fai in modo che il target Swift dipenda 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")

La Swift code poi fa import MyPluginObjC.

La via automatizzata: cap2spm

L'equipe di Ionic capacitor-plugin-converter costruisce un cap2spm binario che legge Plugin.m E e Plugin.h, aggiunge CAPBridgedPlugin conformità alla tua classe Swift, genera Package.swift, sposta i file in Sources E 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

E' progettato per plugin che sono solo Swift, tranne i file di bridge. Eseguielo su un albero Git pulito e poi esegui Step 5 a mano.

Altre opzioni:

  • Scaffolda un plugin fresco con bun create @capacitor/plugin e copia la tua implementazione. Il template supporta già SPM e CocoaPods. Questo è spesso più veloce per plugin piccoli.
  • Usa un agente. La capacitor-plugin-spm-support abilità in Capgo Abilità passa attraverso Package.swift, pulizia del bridge, risorse e package.json. Installa 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 elenchi il tuo pacchetto, e che sincronizzato abbia stampato “Tutti i Capacitor plugin hanno un file Package.swift”. Poi costruisci 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.

Troubleshooting

product 'X' required by package 'capapp-spm' ... not found. Il nome del pacchetto o prodotto non corrisponde al nome derivato dal tuo npm. Consulta la tabella sopra.

"MyPlugin" plugin is not implemented on ios. Ogni Package.swift non era stato pubblicato, jsName non corrisponde registerPlugin, o la classe manca di CAPBridgedPlugin. Verifica la cartella del plugin in node_modules.

Multiple targets named 'Plugin' o errori di modulo duplicati. Due plugin utilizzano lo stesso nome di destinazione. Rinomina il tuo a qualcosa di unico. Gli squadre di app possono anche lavorare attorno a questo con experimental.ios.spm.packageOptions (moduleAliases o symlink) nella Capacitor config, disponibile a partire da CLI 8.4.

target 'X' contains mixed language source files. File Objective-C e Swift condividono una cartella. Dividili in due target.

Xcode mostra errori di pacchetto obsoleti dopo le correzioni. Use File > Pacchetti > Reset Cache Pacchettipoi, ricostruisci nuovamente.

Se hai bisogno anche 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 nel layer web è attivo, invia la correzione attraverso Capgo invece di attendere giorni per l'approvazione della store. Gli utenti ricevono l'aggiornamento in background mentre le modifiche native rimangono nel normale percorso di revisione.

Sostegno umano da Martin

Inizia subito

Ultimi articoli dal nostro Blog

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