Guide to migrating iOS Capacitor plugins and dependencies from CocoaPods to Swift Package Manager (SPM). Use this skill when users want to modernize their iOS project, remove CocoaPods, or add SPM-based dependencies.
Step-by-step guide for migrating Capacitor iOS projects from CocoaPods to Swift Package Manager.
| Aspect | CocoaPods | SPM |
|---|---|---|
| Build Speed | Slower | Faster |
| Apple Integration | Third-party | Native Xcode |
| Ruby Dependency | Required | None |
| Version Management | Podfile.lock | Package.resolved |
| Xcodeproj Changes | Modifies project | Uses workspace |
| Binary Caching | Limited | Built-in |
First, identify what you're currently using:
cd ios/App
cat Podfile
pod outdated
Common Capacitor pods to migrate:
# Podfile (before)
target 'App' do
capacitor_pods
pod 'Firebase/Analytics'
pod 'Firebase/Messaging'
pod 'Alamofire'
pod 'KeychainAccess'
end
Most popular libraries support SPM. Use these URLs:
| Library | SPM URL |
|---|---|
| Firebase | https://github.com/firebase/firebase-ios-sdk |
| Alamofire | https://github.com/Alamofire/Alamofire |
| KeychainAccess | https://github.com/kishikawakatsumi/KeychainAccess |
| SDWebImage | https://github.com/SDWebImage/SDWebImage |
| SnapKit | https://github.com/SnapKit/SnapKit |
| Realm | https://github.com/realm/realm-swift |
| Lottie | https://github.com/airbnb/lottie-spm |
cd ios/App
# Remove CocoaPods integration
pod deintegrate
# Remove Podfile.lock and Pods directory
rm -rf Podfile.lock Pods
# Remove workspace (we'll use project directly or create new workspace)
rm -rf App.xcworkspace
ios/App/App.xcodeproj in XcodeAppCapacitor still needs CocoaPods for its core. Create minimal Podfile:
# ios/App/Podfile
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0'
use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
end
target 'App' do
capacitor_pods
# Other plugin pods that don't support SPM yet
end
post_install do |installer|
assertDeploymentTarget(installer)
end
Then run:
cd ios/App && pod install
If you're creating a Capacitor plugin with SPM support:
Package.swift:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "CapacitorNativeBiometric",
platforms: [.iOS(.v14)],
products: [
.library(
name: "CapacitorNativeBiometric",
targets: ["NativeBiometricPlugin"]
),
],
dependencies: [
.package(url: "https://github.com/nicholasalx/capacitor-swift-pm", from: "6.0.0"),
],
targets: [
.target(
name: "NativeBiometricPlugin",
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
],
path: "ios/Sources/NativeBiometricPlugin",
publicHeadersPath: "include"
),
]
)
ios/
├── App/
│ ├── App/
│ │ ├── AppDelegate.swift
│ │ ├── Info.plist
│ │ └── ...
│ ├── App.xcodeproj/
│ │ └── project.xcworkspace/
│ │ └── xcshareddata/
│ │ └── swiftpm/
│ │ └── Package.resolved # SPM lock file
│ ├── Podfile
│ └── Podfile.lock
└── ...
Most Capacitor projects work best with a hybrid approach:
@capacitor/ios corePodfile:
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0'
use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
# Plugins without SPM support
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
end
target 'App' do
capacitor_pods
# NO Firebase here - use SPM instead
end
Xcode Package Dependencies:
Cause: Same library in both CocoaPods and SPM
Solution: Remove from Podfile if using SPM
# Podfile - WRONG
pod 'Firebase/Analytics' # Remove this
# Use SPM instead in Xcode
Cause: SPM package not linked to target
Solution:
Cause: Missing frameworks or wrong imports
Solution: Clean and rebuild
# Clean derived data
rm -rf ~/Library/Developer/Xcode/DerivedData
# Clean build folder in Xcode
# Cmd + Shift + K
# Rebuild
bunx cap sync ios
cd ios/App && pod install
Cause: Plugin needs registration
Solution: Ensure plugin is registered in AppDelegate.swift:
import Capacitor
import YourPlugin
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Capacitor handles plugin registration automatically
return true
}
}
my-capacitor-plugin/
├── Package.swift
├── ios/
│ └── Sources/
│ └── MyPlugin/
│ ├── MyPlugin.swift
│ ├── MyPlugin.m # Objc bridge if needed
│ └── include/
│ └── MyPlugin.h # Public headers
├── src/
│ ├── index.ts
│ ├── definitions.ts
│ └── web.ts
└── package.json
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "CapacitorMyPlugin",
platforms: [.iOS(.v14)],
products: [
.library(
name: "CapacitorMyPlugin",
targets: ["MyPluginPlugin"]
),
],
dependencies: [
.package(url: "https://github.com/nicholasalx/capacitor-swift-pm", from: "6.0.0"),
],
targets: [
.target(
name: "MyPluginPlugin",
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
],
path: "ios/Sources/MyPlugin"
),
]
)
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),
]
@objc func echo(_ call: CAPPluginCall) {
let value = call.getString("value") ?? ""
call.resolve(["value": value])
}
}
pod deintegratepod installPackage.resolved to git