Perché Flutter sta passando a Swift Package Manager
Per anni la gestione delle dipendenze native su iOS e macOS in Flutter è stata affidata a CocoaPods: un tool Ruby, esterno alla toolchain Apple, che richiede installazione separata, aggiornamenti continui e che è la causa di una quota enorme delle segnalazioni di build rotte (il classico pod install che fallisce dopo un aggiornamento di Xcode o di Ruby).
Swift Package Manager (SPM) è invece integrato direttamente in Xcode e nella toolchain Swift. Il team Flutter ha iniziato la migrazione con l'obiettivo di:
- eliminare la dipendenza da Ruby e CocoaPods nel setup di sviluppo;
- permettere ai plugin di usare l'enorme ecosistema di pacchetti Swift già esistente;
- allinearsi agli strumenti ufficiali Apple, con build più prevedibili.
Il supporto è arrivato in preview per iOS con Flutter 3.24 e si è esteso a macOS nelle versioni successive. CocoaPods non sparisce da un giorno all'altro: i due sistemi convivono, e Flutter usa SPM per i plugin che lo supportano e CocoaPods per tutti gli altri.
Abilitare SPM nel proprio ambiente
Il supporto è dietro un feature flag globale della CLI:
flutter config --enable-swift-package-manager
Per disabilitarlo:
flutter config --no-enable-swift-package-manager
Requisiti tipici: Xcode 15 o superiore, deployment target minimo iOS 12 / macOS 10.14 (SPM non supporta target più bassi per i plugin Flutter).
Un modo veloce per verificare lo stato dei flag:
flutter config --list
flutter doctor -v
Migrare un'app esistente
La parte sorprendente è che non esiste un comando di migrazione dedicato: una volta abilitato il flag, basta eseguire
flutter run
# oppure
flutter build ios
e lo strumento modifica automaticamente il progetto Xcode. In concreto vengono aggiunti:
- una build phase chiamata "Run Prepare Flutter Framework Script" nel target Runner;
- un pacchetto locale generato,
FlutterGeneratedPluginSwiftPackage, posizionato inios/Flutter/ephemeral/Packages/(omacos/Flutter/ephemeral/Packages/), che elenca come dipendenze i plugin SPM-ready; - il riferimento al pacchetto dentro
Runner.xcodeproj.
Le modifiche a Runner.xcodeproj vanno committate, perché fanno parte della configurazione del progetto. La cartella ephemeral è invece già ignorata dal .gitignore standard di Flutter e viene rigenerata ad ogni build.
Da quel momento, per ogni plugin Flutter verifica se esiste un Package.swift: se sì lo integra via SPM, altrimenti continua a passarlo a CocoaPods. Nei log con flutter run -v si leggono righe del tipo "Adding Swift Package Manager integration" e l'elenco dei plugin gestiti da ciascun sistema.
Disattivare SPM per un singolo progetto
Se un'app ha problemi (per esempio script o post_install complessi nel Podfile), si può restare su CocoaPods solo per quel progetto aggiungendo in cima a ios/Podfile:
# Mantieni CocoaPods per questo progetto
disable_swift_package_manager!
platform :ios, '13.0'
Aggiungere il supporto SPM a un plugin
Se mantenete un plugin su pub.dev, aggiungere SPM significa affiancare un Package.swift al .podspec esistente, senza rompere gli utenti ancora su CocoaPods. La struttura attesa è questa:
my_plugin/
└── ios/
├── my_plugin/
│ ├── Package.swift
│ └── Sources/
│ └── my_plugin/
│ ├── MyPlugin.swift
│ └── Resources/
└── my_plugin.podspec
Nota la cartella intermedia con lo stesso nome del plugin: è lì che Flutter cerca il manifest SPM.
Un Package.swift minimale:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "my_plugin",
platforms: [
.iOS("12.0")
],
products: [
// Il nome del prodotto non può coincidere con quello del target:
// per convenzione si usano i trattini.
.library(name: "my-plugin", targets: ["my_plugin"])
],
dependencies: [],
targets: [
.target(
name: "my_plugin",
dependencies: [],
resources: [
.process("Resources")
],
cSettings: [
.headerSearchPath("include/my_plugin")
]
)
]
)
Punti di attenzione:
- Nomi: il target deve chiamarsi come il plugin (snake_case), il prodotto
librarydeve avere un nome diverso (per convenzione con i trattini). - Sorgenti: devono stare in
Sources/<nome_target>/. Se prima erano inios/Classes/, vanno spostate; aggiornate il.podspeccons.source_files = 'my_plugin/Sources/my_plugin/**/*.swift'così i due sistemi puntano agli stessi file. - Risorse: con SPM si accedono tramite
Bundle.module, mentre con CocoaPods servono i bundle classici. Una soluzione comune è un helper condizionale:
extension Bundle {
static var pluginBundle: Bundle {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: MyPlugin.self)
#endif
}
}
- Dipendenze native di terze parti: una libreria disponibile come Swift Package si dichiara in
dependencies, mentre un framework binario si integra con.binaryTarget(name:path:)puntando a un.xcframework. - Registrazione del plugin: non cambia nulla lato Dart né lato registrazione; il
FlutterPlugincontinua a esporreregister(with:)come sempre.
public class MyPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "my_plugin",
binaryMessenger: registrar.messenger()
)
let instance = MyPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}
E dal lato Dart nulla cambia:
import 'package:flutter/services.dart';
class MyPlugin {
static const _channel = MethodChannel('my_plugin');
static Future<String?> getPlatformVersion() {
return _channel.invokeMethod<String>('getPlatformVersion');
}
}
Testare entrambe le modalità
Prima di pubblicare, conviene verificare che l'app di esempio compili in tutti e due gli scenari:
# Con SPM attivo
flutter config --enable-swift-package-manager
cd example && flutter clean && flutter build ios --no-codesign
# Con CocoaPods
flutter config --no-enable-swift-package-manager
cd example && flutter clean && flutter build ios --no-codesign
Nei test CI dei plugin ufficiali questa doppia matrice è la norma: è l'unico modo per non rompere gli utenti che non hanno ancora abilitato il flag.
Impatti su CI/CD
Alcune accortezze quando spostate la build su una pipeline:
- Abilitate il flag nel job, altrimenti la build userà CocoaPods anche se il progetto è già migrato: aggiungete
flutter config --enable-swift-package-managerprima diflutter build. - Rete: la risoluzione dei pacchetti SPM scarica dipendenze remote. Se usate runner con rete limitata, può servire un mirror o una cache.
- Cache: i pacchetti risolti finiscono nella DerivedData e in
~/Library/Caches/org.swift.swiftpm. Cachearli riduce sensibilmente i tempi. - Risoluzione anticipata: per fallire presto e in modo leggibile si può lanciare
xcodebuild -resolvePackageDependencies -workspace ios/Runner.xcworkspace -scheme Runner
- Finché esiste almeno un plugin solo-CocoaPods, CocoaPods resta necessario sul runner: la migrazione completa dell'ecosistema richiederà tempo.
Troubleshooting dei problemi più comuni
- "Couldn't find package" / pacchetto non risolto: in Xcode usate File → Packages → Reset Package Caches, poi
flutter cleane ricompilate. - Deployment target troppo basso: se il progetto punta a iOS 11, SPM si rifiuta di integrare i plugin. Alzate
platform :ios, '13.0'nel Podfile eIPHONEOS_DEPLOYMENT_TARGETnelle build settings. - Plugin non trovato dopo la migrazione: controllate che la build phase "Run Prepare Flutter Framework Script" sia presente nel target Runner e che
ios/Flutter/ephemeral/Packages/venga rigenerata. - Progetti Xcode molto personalizzati: se avete script custom o target multipli, valutate di restare temporaneamente su CocoaPods con
disable_swift_package_manager!e migrare in un secondo momento. - Risorse mancanti a runtime: quasi sempre è il discorso
Bundle.modulevsBundle(for:)visto sopra.
Conviene migrare adesso?
Per un'app in produzione la migrazione è a basso rischio ma non urgente: potete abilitare il flag su un branch, verificare che tutti i plugin usati compilino e misurare i tempi di build. Il rollback è immediato (--no-enable-swift-package-manager o disable_swift_package_manager!).
Per chi mantiene un plugin, invece, conviene agire subito: aggiungere un Package.swift accanto al .podspec è retrocompatibile al 100% e, man mano che il flag diventerà il default, eviterà agli utenti di dover installare CocoaPods solo per causa vostra.
La direzione è chiara: in un futuro non troppo lontano un nuovo progetto Flutter per iOS e macOS si compilerà senza Ruby, senza Podfile e senza pod install. Arrivarci con anticipo significa evitare la fretta quando il cambio diventerà obbligatorio.