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:

  1. una build phase chiamata "Run Prepare Flutter Framework Script" nel target Runner;
  2. un pacchetto locale generato, FlutterGeneratedPluginSwiftPackage, posizionato in ios/Flutter/ephemeral/Packages/ (o macos/Flutter/ephemeral/Packages/), che elenca come dipendenze i plugin SPM-ready;
  3. 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 library deve avere un nome diverso (per convenzione con i trattini).
  • Sorgenti: devono stare in Sources/<nome_target>/. Se prima erano in ios/Classes/, vanno spostate; aggiornate il .podspec con s.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 FlutterPlugin continua a esporre register(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-manager prima di flutter 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 clean e 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 e IPHONEOS_DEPLOYMENT_TARGET nelle 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.module vs Bundle(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.