Back to all posts

How I Migrated My Flutter App From CocoaPods to Swift Package Manager

Sixtus Miracle AgboSixtus Miracle Agbo
6 min read
How I Migrated My Flutter App From CocoaPods to Swift Package Manager

Every iOS build of FoodPilot was printing two warnings I kept scrolling past:

The following plugins do not support Swift Package Manager for ios:
  - app_tracking_transparency
  - flutter_local_notifications
  - google_mobile_ads
  - sign_in_with_apple
This will become an error in a future version of Flutter. Please contact the plugin maintainers to request Swift Package Manager adoption.
[!] FirebaseCore has been deprecated in favor of the Firebase Apple SDK via Swift Package Manager. Existing CocoaPods versions will remain available and installations will remain functional, but new versions will no longer be published to CocoaPods after October 2026.

Flutter is moving iOS and macOS dependencies from CocoaPods to Swift Package Manager (SwiftPM), Firebase is dropping CocoaPods for new releases, and Flutter's tooling already says turning SwiftPM off "will not be allowed in a future version of Flutter." So I finally migrated.

FoodPilot is a real app on the App Store, not a counter demo. It uses Firebase (Auth, Firestore, Functions, Messaging, Crashlytics, Analytics), Google Mobile Ads, RevenueCat, Sign in with Apple, Google Sign-In, and local notifications. That is a lot of native code, and four things broke on the way. This post is exactly what I did, including the failures, so you can skip them.

What I was working with

  • Flutter 3.47.1 (stable)
  • Xcode 26.4
  • CocoaPods 1.16.2
  • iOS deployment target 15.0

Version numbers will move after this post. The failures are the useful part, because they come from how the migration works, not from one specific version.

How Flutter's Swift Package Manager support works

When SwiftPM is on, Flutter generates a local Swift package called FlutterGeneratedPluginSwiftPackage and adds it to your Xcode project. Every plugin that ships a Package.swift gets linked through it. Plugins that do not support SwiftPM yet keep installing through CocoaPods, so in theory both run side by side while you migrate.

"In theory" is doing some work in that sentence. More on that below.

Step 1: Work on a branch

The migration edits your Xcode project, your scheme, and probably your Dart code. I did all of it on a separate branch in a git worktree, so my main checkout stayed buildable the whole time:

git worktree add -b spm-migration ../foodpilot-spm
cd ../foodpilot-spm

A normal branch works too. The point is being able to throw it away.

Step 2: Turn SwiftPM on for the project

SwiftPM is on by default in current Flutter, but FoodPilot was still fully on CocoaPods. The reason was my machine:

flutter config --list
  enable-swift-package-manager: false

At some point I had turned it off globally and forgotten.

Flutter checks two places for this setting, in order: the project's pubspec.yaml first, then the global flutter config. The project setting wins. That means you can migrate one app without touching the rest, which is what I did:

flutter:
  config:
    enable-swift-package-manager: true

If you want every project on SwiftPM, flutter config --enable-swift-package-manager flips the global setting instead.

Step 3: Clean and build

I cleared out the old CocoaPods state, then did a release build, since release is what ships:

flutter clean
rm -rf ios/Pods ios/.symlinks ios/Podfile.lock
flutter build ios --release --no-codesign

Early in the output, Flutter ran the migration:

Adding Swift Package Manager integration...                       120.3s

Two minutes, mostly Xcode fetching packages. It changed three things:

  • ios/Runner.xcodeproj/project.pbxproj now references FlutterGeneratedPluginSwiftPackage.
  • Runner.xcscheme got a new build pre-action called "Run Prepare Flutter Framework Script".
  • New Package.resolved files appeared under ios/Runner.xcworkspace/xcshareddata/swiftpm/ and ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/. They pin the exact native SDK versions (for FoodPilot, firebase-ios-sdk 12.18.0, RevenueCat, Google Sign-In, and more). Commit them, the same way you committed Podfile.lock.

Then the build failed.

Problem 1: The hybrid setup broke pod install

[!] Unable to find a specification for `webview_flutter_wkwebview` depended upon by `google_mobile_ads`

This one surprised me, because the whole promise of the hybrid setup is that CocoaPods plugins keep working. Here is what happened.

google_mobile_ads 5.3.1 did not support SwiftPM, so it still went through CocoaPods. But it depends on webview_flutter_wkwebview, which does support SwiftPM. Flutter moved webview_flutter_wkwebview out of the Podfile and into the Swift package, so when CocoaPods tried to resolve google_mobile_ads, its dependency was gone.

The takeaway: if a CocoaPods-only plugin depends on another plugin that has already moved to SwiftPM, the hybrid setup can fail. The fix is to get that plugin onto SwiftPM too.

Step 4: Upgrade the plugins on the list

I checked each plugin from the warning on pub.dev. All four had added SwiftPM support, just in newer versions than FoodPilot used:

PluginFoodPilot hadSwiftPM support added inUpgraded to
app_tracking_transparency2.0.6+12.0.72.0.7
flutter_local_notifications18.0.119.0.022.3.1
google_mobile_ads5.3.18.0.09.1.0
sign_in_with_apple6.1.48.0.08.2.0

The changelog tells you. Search it for "Swift Package Manager" or "SPM".

I did not run flutter pub upgrade --major-versions across the whole app. That bumps everything at once, and if something breaks you do not know which package caused it. You can pass package names to upgrade only those:

flutter pub upgrade --major-versions app_tracking_transparency flutter_local_notifications google_mobile_ads sign_in_with_apple

Problem 2: Version solving failed

Because foodpilot depends on flutter_local_notifications ^22.3.1 which depends on timezone ^0.11.0, timezone ^0.11.0 is required.
So, because foodpilot depends on timezone ^0.9.4, version solving failed.

If you schedule notifications with flutter_local_notifications, you probably depend on timezone directly too, and the new version needs a newer one. Adding it to the same command fixed it:

flutter pub upgrade --major-versions app_tracking_transparency flutter_local_notifications google_mobile_ads sign_in_with_apple timezone

Problem 3: Breaking API changes in flutter_local_notifications

Going from 18 to 22 crosses several major versions. flutter analyze found 14 errors, all in my notification service. Two changes caused all of them:

  • Version 20 converted positional parameters to named parameters in initialize(), show(), periodicallyShow(), periodicallyShowWithDuration(), cancel(), and zonedSchedule().
  • Version 19 removed uiLocalNotificationDateInterpretation from zonedSchedule(), along with the UILocalNotificationDateInterpretation enum.

Before:

await _localNotifications.initialize(
  initSettings,
  onDidReceiveNotificationResponse: _onNotificationResponse,
);
 
await _localNotifications.zonedSchedule(
  id,
  title,
  body,
  scheduledDate,
  notificationDetails,
  androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
  uiLocalNotificationDateInterpretation:
      UILocalNotificationDateInterpretation.absoluteTime,
  matchDateTimeComponents: DateTimeComponents.time,
);
 
await _localNotifications.cancel(id);

After:

await _localNotifications.initialize(
  settings: initSettings,
  onDidReceiveNotificationResponse: _onNotificationResponse,
);
 
await _localNotifications.zonedSchedule(
  id: id,
  title: title,
  body: body,
  scheduledDate: scheduledDate,
  notificationDetails: notificationDetails,
  androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
  matchDateTimeComponents: DateTimeComponents.time,
);
 
await _localNotifications.cancel(id: id);

The google_mobile_ads jump from 5 to 9 and the sign_in_with_apple jump from 6 to 8 did not break anything in my code. Run flutter analyze anyway, because your usage may differ.

Problem 4: The Crashlytics build phase pointed at Pods

After the upgrades, the build printed a new message:

All plugins found for ios are Swift Packages, but your project still has CocoaPods integration. Your project uses a non-standard Podfile and will need to be migrated to Swift Package Manager manually.

Good news and a scary word. I diffed my Podfile against Flutter's template to see what "non-standard" meant. The only difference was one line:

platform :ios, '15.0'

In Flutter's template that line is commented out. Flutter compares the whole file for an exact match, so uncommenting the platform line, which a lot of apps do, is enough to get labeled non-standard. My deployment target was already set to 15.0 in the Xcode project, so the line was redundant anyway.

The build then failed with this:

.../ios/Pods/FirebaseCrashlytics/run: No such file or directory
Command PhaseScriptExecution failed with a nonzero exit code

FoodPilot has an "Upload Crashlytics dSYMs" run script build phase, and it called the CocoaPods copy of the script:

"${PODS_ROOT}/FirebaseCrashlytics/run"

Crashlytics now comes from the Swift package, so that file no longer exists. Firebase's own SwiftPM docs give the new path. In Xcode, open the Runner target, go to Build Phases, find the Crashlytics script, and replace it with:

"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"

That path works for both flutter build from the terminal and builds from inside Xcode. Check for any other run scripts that reference PODS_ROOT while you are in there.

Step 5: Remove CocoaPods

With every plugin on SwiftPM, CocoaPods had nothing left to do. Because Flutter had labeled my Podfile non-standard, it did not give me the one-line cleanup, so I did it by hand:

cd ios
pod deintegrate
rm Podfile Podfile.lock

pod deintegrate removes the Pods frameworks, xcconfig references, and the "Check Pods Manifest.lock" build phase from the Xcode project. It ends with a note that is easy to miss:

Project has been deintegrated. No traces of CocoaPods left in project.
Note: The workspace referencing the Pods project still remains.

So there were two more things to clean up. First, ios/Flutter/Debug.xcconfig and ios/Flutter/Release.xcconfig each still had a CocoaPods include at the top. I deleted these lines:

#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"

Second, ios/Runner.xcworkspace/contents.xcworkspacedata still referenced the Pods project. I removed this entry:

<FileRef
   location = "group:Pods/Pods.xcodeproj">
</FileRef>

pod deintegrate also left an empty "Pods" group in the project navigator. It does nothing, and you can delete it in Xcode if it bothers you.

Step 6: Build again

flutter clean
flutter build ios --release --no-codesign
Xcode build done.                                           174.3s
✓ Built build/ios/iphoneos/Runner.app (68.8MB)

No plugin warning, no CocoaPods step, no pod install in the output at all.

I also ran a debug build on the iOS simulator to make sure the plugins actually load at runtime, not just link. The app opened to the sign-in screen, and the device logs showed Firebase Crashlytics and Analytics starting at version 12.18.0, the version pinned in Package.resolved, along with the Google Mobile Ads SDK.

The final diff was 10 files, with 101 lines added and 1,946 removed. Most of the removals were Podfile.lock.

Step 7: Test on a real device

A green build proves the app compiles and links. A simulator run proves the plugins load. Neither proves every native SDK behaves on real hardware. Debug and release builds optimize native code differently, and I have had a Firebase call that worked in debug abort with SIGABRT in release.

So the last check was my own iPhone:

flutter run --release -d <your-device>

It worked. On the device, go through every path that touches native code: sign-in with Apple and Google, push notifications, scheduled local notifications and their actions, ads and the tracking prompt, purchases, and anything calling Firebase. For a migration like this, that is the test that matters, and it is worth a round on TestFlight before the update reaches users.

If you are blocked and a release is due, you can put the app back on CocoaPods by setting enable-swift-package-manager: false in its pubspec.yaml. Just know it is a temporary way out. Flutter already says that option is going away, and every SDK that moves to SwiftPM only makes the eventual migration bigger.

Cover photo by Ion (Ivan) Sipilov on Unsplash

Share this post
Sixtus Miracle Agbo

Sixtus Miracle Agbo

Full-Stack Developer crafting high-performance web and mobile applications. I write about software development, technology, and lessons learned building real products.

Get in touch

Subscribe to my newsletter

New posts on web & mobile development, straight to your inbox. No spam, unsubscribe anytime.