Langsung ke konten utama

Migrasi Plugin Capacitor ke Swift Package Manager

Tambahkan dukungan Swift Package Manager untuk plugin Capacitor: aturan penamaan Package.swift, CAPBridgedPlugin, tata letak Sources, resource, Obj-C code, dan cap2spm.

Ilustrasi migrasi plugin Capacitor dari CocoaPods ke Swift Package Manager

Untuk memigrasikan plugin Capacitor ke Swift Package Manager, tambahkan Package.swift di root plugin yang nama package dan product-nya sesuai dengan yang diharapkan Capacitor CLI, pindahkan iOS code ke dalam ios/Sources/<Target>, ganti file bridge Objective-C CAP_PLUGIN bridge file dengan CAPBridgedPlugin conformance di Swift, lalu tambahkan Package.swift ke npm files . Tetap simpan podspec agar aplikasi CocoaPods tetap berjalan.

Sejak Capacitor 8, cap add ios secara default membuat proyek SPM. Plugin tanpa Package.swift akan dilewati di aplikasi tersebut dengan peringatan “Some installed Capacitor plugins are not compatible with SPM”, lalu pengguna melihat “plugin is not implemented” saat runtime. Panduan ini membahas migrasi manual, alat konverter, dan bagian yang sering dilewati panduan lain: aturan penamaan, resource, Objective-C code, dan dependensi pihak ketiga.

Tim aplikasi yang memigrasikan proyeknya sendiri sebaiknya membaca Cara migrasi aplikasi Capacitor Anda ke SPM instead.

Cara Capacitor menggunakan package Anda

Di bunx cap sync ios, CLI menulis ulang ios/App/CapApp-SPM/Package.swift di dalam aplikasi. Untuk setiap plugin Capacitor yang memiliki Package.swift, ia menambahkan dua baris:

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

Dua konsekuensinya:

  1. Package Anda dipakai via local path dari node_modules, bukan dari URL git. Anda Package.swift harus berada di root package npm, dan semua referensinya harus dipublikasikan ke npm.
  2. Namanya diambil dari nama npm, bukan dari podspec Anda. CLI menghapus @, mengubah / dan - ke dalam _, lalu ubah tiap segmen ke camelCase dan kapitalkan huruf pertamanya:
paket npm Nama paket dan produk yang wajib
@capacitor/haptics CapacitorHaptics
@capgo/capacitor-updater CapgoCapacitorUpdater
capacitor-my-plugin CapacitorMyPlugin
@acme/capacitor-scanner AcmeCapacitorScanner

Jika Package.swift menggunakan nama atau nama produk lain, aplikasi gagal me-resolve paket dengan error seperti product 'X' required by package 'capapp-spm' target 'CapApp-SPM' not found.

Langkah 1: tulis Package.swift

Ini adalah layout yang digunakan plugin resmi. Berikut adalah manifest aslinya dari @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")
    ]
)

Catatan:

  • Pertahankan swift-tools-version: 5.9 kecuali jika Anda butuh fitur yang lebih baru. Paket yang di-generate aplikasi juga menggunakan 5.9 secara default, dan Capacitor 8 belum mendukung Swift 6 secara resmi.
  • Gunakan from: "8.0.0" untuk capacitor-swift-pm. Aplikasi mengunci versi persis yang cocok dengan versi terpasangnya @capacitor/ios, dan rentang memungkinkan SPM menyelesaikan keduanya. branch: atau exact: akan menyebabkan konflik resolusi.
  • Nama target bisa apa saja, tetapi akan menjadi nama modul Swift. Hindari nama generik seperti Plugin yang bentrok dengan plugin lain.

Langkah 2: pindahkan source

SPM mengharuskan satu folder per target. Converter dan template resmi menggunakan:

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

Kemudian hapus apa yang tidak dibutuhkan SPM: ios/Plugin.xcodeproj, ios/Plugin.xcworkspace, ios/Podfile, ios/Plugin/Info.plist dan ios/PluginTests/Info.plist.

Perbarui podspec agar CocoaPods mengompilasi file yang sama:

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'

Langkah 3: ganti bridge Objective-C dengan CAPBridgedPlugin

Plugin lama mendaftarkan metode di Plugin.m:

#import <Capacitor/Capacitor.h>

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

SwiftPM tidak bisa mencampur Objective-C dan Swift dalam satu target, jadi ini dipindahkan ke kelas 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
        // ...
    }
}

Aturan pemetaan:

  • identifier adalah argumen pertama dari CAP_PLUGIN (nama kelas).
  • jsName adalah argumen kedua, nama yang digunakan di registerPlugin('MyPlugin') di TypeScript Anda.
  • Setiap CAP_PLUGIN_METHOD menjadi satu CAPPluginMethod, dengan tipe return yang sama (CAPPluginReturnPromise, CAPPluginReturnCallback atau CAPPluginReturnNone).

Metode yang hilang dari pluginMethods terkompilasi dengan baik dan hanya gagal saat dipanggil dari JavaScript. Grep untuk @objc func dengan CAPPluginCall parameter dan hitung terhadap array. Lalu hapus Plugin.h dan Plugin.m.

CAPBridgedPlugin juga bekerja dengan CocoaPods, jadi file Swift yang sama bisa dipakai untuk kedua package manager.

Langkah 4: perbarui package.json dan .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"
  }
}

Panduan -scheme adalah nama package Anda. Lupa Package.swift atau ios/Sources di files adalah alasan paling umum mengapa plugin berfungsi dari git checkout tetapi rusak setelah npm publish. Jalankan bun pm pack --dry-run dan periksa daftar filenya.

Tambahkan output build SPM ke .gitignore:

.build/
Package.resolved
/Packages
.swiftpm/

Langkah 5: tangani yang tak bisa dilakukan converter

Dependensi pihak ketiga

Baris podspec seperti s.dependency 'Alamofire', '~> 5.9' menjadi dependensi package dan product pada target Anda:

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

Jika SDK hanya menyediakan .xcframework, masukkan ke dalam package npm dan gunakan binary target:

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

Jika vendor tidak menyediakan package SPM maupun xcframework, Anda belum bisa menambahkan dukungan SPM. Tetap gunakan CocoaPods untuk plugin tersebut dan cantumkan di README.

Pastikan rentang versi di Package.swift dan podspec tetap selaras. Dua plugin dalam satu app yang membutuhkan versi SDK yang tidak kompatibel akan menyebabkan error resolusi di package manager mana pun.

Resource dan privacy manifest

Gambar, JSON, storyboard, dan PrivacyInfo.xcprivacy harus dideklarasikan:

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

CocoaPods membutuhkan file yang sama untuk dideklarasikan di podspec. Letakkan file itu dalam resource bundle bernama:

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

Di code, resource SPM berada di Bundle.module, sedangkan CocoaPods meletakkannya di MyPluginResources.bundle, di samping kelas plugin. Ganti dengan SWIFT_PACKAGE flag, yang secara otomatis didefinisikan oleh SwiftPM:

#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

Objective-C code asli

Jika sebagian plugin menggunakan Objective-C (bukan hanya bridge), letakkan di target terpisah dengan public headers di include folder, dan buat target Swift bergantung padanya:

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

Swift code kemudian melakukan import MyPluginObjC.

Jalur otomatis: cap2spm

dari tim Ionic capacitor-plugin-converter membangun cap2spm binary yang membaca Plugin.m dan Plugin.h, menambahkan CAPBridgedPlugin conformance ke Swift class Anda, menghasilkan Package.swift, memindahkan file ke Sources dan Tests, memperbarui podspec dan package.json, serta menghapus file project Xcode lama.

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

Didesain untuk plugin yang hanya menggunakan Swift di luar file bridge. Jalankan di tree git yang bersih, lalu lakukan Langkah 5 secara manual.

Opsi lain:

  • Scaffold plugin baru dengan bun create @capacitor/plugin dan salin implementasi Anda ke dalamnya. Template sudah mendukung SPM dan CocoaPods. Ini sering kali lebih cepat untuk plugin kecil.
  • Gunakan agen. Panduan capacitor-plugin-spm-support skill di Skill Capgo yang open menjelaskan Package.swift, pembersihan bridge, resources, dan package.json. Instal dengan bunx skills add Cap-go/capgo-skills.

Tes di aplikasi SPM asli

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

Pastikan bahwa ios/App/CapApp-SPM/Package.swift mencantumkan package Anda, dan bahwa sync mencetak “All Capacitor plugins have a Package.swift file”. Lalu build di perangkat dan panggil setiap method. Ulangi di app yang dibuat dengan bunx cap add ios --packagemanager CocoaPods untuk memastikan podspec masih berfungsi.

Pemecahan Masalah

product 'X' required by package 'capapp-spm' ... not found. Nama Package atau produk tidak cocok dengan nama yang diturunkan dari nama npm Anda. Lihat tabel di atas.

"MyPlugin" plugin is not implemented on ios. Entah Package.swift tidak dipublikasikan, jsName tidak cocok registerPlugin, atau class tersebut tidak memiliki CAPBridgedPlugin. Periksa folder plugin di node_modules.

Multiple targets named 'Plugin' atau error module duplikat. Dua plugin menggunakan nama target yang sama. Ubah nama Anda menjadi nama yang unik. Tim aplikasi juga bisa mengatasinya dengan experimental.ios.spm.packageOptions (moduleAliases atau symlink) di konfigurasi Capacitor, tersedia sejak CLI 8.4.

target 'X' contains mixed language source files. File Objective-C dan Swift berbagi folder. Pisahkan menjadi dua target.

Xcode menampilkan error paket yang sudah usang setelah perbaikan. Gunakan File > Packages > Reset Package Caches, lalu build lagi.

Jika Anda juga membutuhkan fitur opsional yang di-toggle per aplikasi, baca cara menggunakan SPM package traits di Capacitor. For apps that must stay on CocoaPods for now, see cara menggunakan CocoaPods dengan Capacitor 8, dan untuk gambaran besarnya, SPM vs CocoaPods untuk Capacitor.

Live updates untuk aplikasi Capacitor

Saat bug web-layer sudah live, kirim perbaikannya lewat Capgo alih-alih menunggu berhari-hari untuk persetujuan app store. Pengguna mendapat update di background, sementara perubahan native tetap mengikuti jalur review normal.

Terbaru dari Blog kami

Capgo memberikan insight terbaik yang Anda butuhkan untuk membuat aplikasi mobile yang benar-benar profesional.