Saltar al contenido principal
Capacitor iOS Migración

Migra un plugin de Capacitor a Swift Package Manager

Añade soporte de Swift Package Manager a un plugin de Capacitor: reglas de nombrado en Package.swift, CAPBridgedPlugin, estructura de Sources, recursos, code de Obj-C y cap2spm.

Migra un plugin de Capacitor a Swift Package Manager

Para migrar un plugin de Capacitor a Swift Package Manager, añade un Package.swift en la raíz del plugin cuyo nombre de package y product coincida con lo que espera Capacitor CLI, mueve el code de iOS a ios/Sources/<Target>, reemplaza los archivos de Objective-C CAP_PLUGIN archivos bridge con CAPBridgedPlugin conforme a Swift, y añade Package.swift a la lista de npm files Mantén el podspec para que las apps con CocoaPods sigan funcionando.

Desde Capacitor 8, cap add ios crea proyectos SPM por defecto. Un plugin sin Package.swift se omite en esas apps con el aviso “Some installed Capacitor plugins are not compatible with SPM”, y los usuarios verán “plugin is not implemented” en runtime. Esta guía cubre la migración manual, la herramienta de conversión y lo que la mayoría de guías omiten: reglas de nombrado, recursos, code de Objective-C y dependencias de terceros.

Los equipos de apps que migren su propio proyecto deben leer Cómo migrar tu app Capacitor a SPM en su lugar.

Cómo Capacitor consume tu package

En bunx cap sync ios, las reescrituras de CLI ios/App/CapApp-SPM/Package.swift en la app. Por cada plugin Capacitor que tenga un Package.swift, añade dos líneas:

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

Dos consecuencias:

  1. Tu paquete se consume mediante una ruta local desde node_modules, no desde una URL de git. Tu Package.swift debe estar en la raíz del paquete npm, y todo lo que referencie debe publicarse en npm.
  2. El nombre se deriva del nombre de npm, no de tu podspec. El CLI elimina @, convierte / y - en _, luego aplica camelCase a cada segmento y capitaliza la primera letra:
paquete npm Nombre de paquete y producto requerido
@capacitor/haptics CapacitorHaptics
@capgo/capacitor-updater CapgoCapacitorUpdater
capacitor-my-plugin CapacitorMyPlugin
@acme/capacitor-scanner AcmeCapacitorScanner

Si tu Package.swift usa otro nombre o producto, la app no podrá resolver los paquetes con un error como product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.

Paso 1: escribe Package.swift

Este es el layout que usan los plugins oficiales. Aquí está el manifiesto real de @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")
    ]
)

Notas:

  • Mantén swift-tools-version: 5.9 a menos que necesites una función más reciente. El paquete generado de la app también usa 5.9 por defecto, y Capacitor 8 aún no soporta Swift 6 oficialmente.
  • Usa from: "8.0.0" para capacitor-swift-pm. La app fija una versión exacta que coincide con su @capacitor/ios, y un rango permite que SPM resuelva ambos. branch: o exact: causará conflictos de resolución.
  • El nombre del target puede ser cualquiera, pero se convierte en el nombre del módulo Swift. Evita nombres genéricos como Plugin que colisionen con otros plugins.

Paso 2: mover las fuentes

SPM espera una carpeta por target. El conversor y la plantilla oficial usan:

my-plugin/
├── Package.swift
├── MyPlugin.podspec
├── ios/
│   ├── Sources/
│   │   └── MyPlugin/
│   │       ├── MyPlugin.swift
│   │       └── MyPluginImplementation.swift
│   └── Tests/
│       └── MyPluginTests/
│           └── MyPluginTests.swift
└── package.json

Luego elimina lo que SPM no necesite: ios/Plugin.xcodeproj, ios/Plugin.xcworkspace, ios/Podfile, ios/Plugin/Info.plist y ios/PluginTests/Info.plist.

Actualiza el podspec para que CocoaPods compile los mismos archivos:

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'

Paso 3: reemplaza el bridge de Objective-C con CAPBridgedPlugin

Los plugins antiguos registran métodos en Plugin.m:

#import <Capacitor/Capacitor.h>

CAP_PLUGIN(MyPlugin, "MyPlugin",
    CAP_PLUGIN_METHOD(echo, CAPPluginReturnPromise);
    CAP_PLUGIN_METHOD(startScan, CAPPluginReturnCallback);
)

SwiftPM no permite mezclar Objective-C y Swift en un mismo target, así que esto se mueve a la clase 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
        // ...
    }
}

Reglas de mapeo:

  • identifier es el primer argumento de CAP_PLUGIN (el nombre de la clase).
  • jsName es el segundo argumento, el nombre usado en registerPlugin('MyPlugin') en tu TypeScript.
  • Cada CAP_PLUGIN_METHOD se convierte en un CAPPluginMethod, manteniendo el mismo tipo de retorno (CAPPluginReturnPromise, CAPPluginReturnCallback o CAPPluginReturnNone).

Un método que falta en pluginMethods compila bien y solo falla cuando JavaScript lo llama. Busca con @objc func con un CAPPluginCall parámetro y compáralos con el array. Luego elimina Plugin.h y Plugin.m.

CAPBridgedPlugin también funciona con CocoaPods, así que el mismo archivo Swift sirve para ambos gestores de paquetes.

Paso 4: actualizar package.json y .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"
  }
}

El -scheme valor es el nombre de tu paquete. Olvidar Package.swift o ios/Sources en files es la razón más común por la que un plugin funciona con un git checkout y falla tras npm publish. Ejecuta bun pm pack --dry-run y revisa la lista de archivos.

Añade el output de build de SPM a .gitignore:

.build/
Package.resolved
/Packages
.swiftpm/

Paso 5: gestiona lo que el conversor no hace

Dependencias de terceros

Una línea de podspec como s.dependency 'Alamofire', '~> 5.9' se convierte en una dependencia de paquete y un producto en tu 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")
]

Si el SDK solo distribuye un .xcframework, inclúyelo dentro del paquete npm y usa un binary target:

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

Si un proveedor no ofrece un paquete SPM ni un xcframework, aún no puedes añadir soporte para SPM. Mantén el plugin solo con CocoaPods y acláralo en el README.

Mantén los rangos de versión en Package.swift y el podspec alineados. Dos plugins en la misma app que requieran versiones incompatibles de un SDK fallarán al resolver en cualquier package manager.

Recursos y manifiestos de privacidad

Imágenes, JSON, storyboards y PrivacyInfo.xcprivacy deben declararse:

.target(
    name: "MyPlugin",
    dependencies: [/* ... */],
    path: "ios/Sources/MyPlugin",
    resources: [
        .process("Resources"),
        .copy("PrivacyInfo.xcprivacy")
    ])

CocoaPods requiere declarar los mismos archivos en el podspec. Ponlos en un bundle de recursos con nombre:

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

En code, los recursos de SPM están en Bundle.module, mientras que CocoaPods los pone en ese MyPluginResources.bundle, junto a la clase del plugin. Cambia con el SWIFT_PACKAGE flag, que SwiftPM define automáticamente:

#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

El code real de Objective-C

Si parte del plugin es Objective-C (no solo el bridge), ponlo en su propio target con headers públicos en una include carpeta, y haz que el target de Swift dependa de él:

.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")

El code de Swift entonces hace import MyPluginObjC.

La ruta automatizada: cap2spm

Del equipo de Ionic capacitor-plugin-converter construye un cap2spm binario que lee Plugin.m y Plugin.h, añade CAPBridgedPlugin conformance a tu clase Swift, genera Package.swift, mueve los archivos a Sources y Tests, actualiza el podspec y package.json, y elimina los archivos antiguos del proyecto 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

Está diseñado para plugins que son solo Swift, salvo los archivos del bridge. Ejecútalo en un git tree limpio y luego haz el Paso 5 manualmente.

Otras opciones:

  • Generar un plugin nuevo con bun create @capacitor/plugin y copiar tu implementación. La plantilla ya soporta SPM y CocoaPods. Suele ser más rápido para plugins pequeños.
  • Usa un agente. El capacitor-plugin-spm-support skill en Habilidades de Capgo explica Package.swift, la limpieza del bridge, recursos y package.json. Instálalo con bunx skills add Cap-go/capgo-skills.

Prueba en una app real con SPM

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 que ios/App/CapApp-SPM/Package.swift liste tu paquete y que el sync haya impreso “All Capacitor plugins have a Package.swift file”. Luego, compila en un dispositivo y llama a cada método. Repite el proceso en una app creada con bunx cap add ios --packagemanager CocoaPods para confirmar que el podspec sigue funcionando.

Solución de problemas

product 'X' required by package 'capapp-spm' ... not found. El nombre del Package o producto no coincide con el nombre derivado de tu npm. Consulta la tabla anterior.

"MyPlugin" plugin is not implemented on ios. Ya sea que Package.swift no se haya publicado, jsName no coincida con registerPlugin, o que a la clase le falte CAPBridgedPlugin. Inspecciona la carpeta del plugin en node_modules.

Multiple targets named 'Plugin' o errores de módulos duplicados. Dos plugins usan el mismo nombre de target. Cambia el tuyo por uno único. Los equipos de apps también pueden solucionarlo con experimental.ios.spm.packageOptions (moduleAliases o symlink) en la configuración de Capacitor, disponible desde CLI 8.4.

target 'X' contains mixed language source files. Los archivos Objective-C y Swift comparten carpeta. Divídelos en dos targets.

Xcode muestra errores de paquetes obsoletos tras los cambios. Usa Archivo &gt; Paquetes &gt; Restablecer caché de paquetes, y vuelve a compilar.

Si también necesitas activar funciones opcionales por app, lee cómo usar package traits de SPM en Capacitor. Para apps que deban seguir en CocoaPods por ahora, mira cómo usar CocoaPods con Capacitor 8, y para una visión general, SPM vs CocoaPods para Capacitor.

Live Updates para apps de Capacitor

Si hay un bug en la capa web, publica el fix con Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el proceso de revisión normal.

Lo último de nuestro blog

Capgo te da los mejores insights para crear una app móvil realmente profesional.