Your Flutter project now changes the iOS workspace after dependency resolution, or a plugin disappears from the build.
Keep CocoaPods for incompatible plugins, adopt Swift Package Manager as the default path, and do not replace the production build machine until an isolated Mac completes Archive and TestFlight validation.
This guide is for you if you are upgrading an existing Flutter project to 3.47 and cannot afford a broken iOS release. It also fits projects with several native plugins whose Swift Package Manager support is unclear, and teams that develop on Windows or Linux but use a remote Mac for iOS builds.
Last updated September 17, 2026. The version and dependency behavior were checked against the Flutter release notes, the Swift Package Manager migration documentation, and the Flutter iOS deployment guide.
The migration decision
Flutter 3.47 uses Swift Package Manager as the default direction for iOS and macOS native dependencies. That does not make CocoaPods immediately obsolete. The safe choice is a dual-track migration: move compatible plugins to SwiftPM, keep incompatible plugins on CocoaPods, and prevent the same native library from being introduced by both systems.
The release chain should remain unchanged until the new chain passes these checks:
- A clean dependency resolution completes.
- A no-signing simulator or device build compiles the native plugins.
- A Release Archive completes in the isolated environment.
- The exported build has the expected signing identity and provisioning profile.
- TestFlight accepts the upload and the critical native features work after installation.
Flutter documentation states that SwiftPM became the default dependency management approach from the Flutter 3.44 line, while dependencies that are not yet compatible can fall back to CocoaPods. Check the Flutter SwiftPM app developer documentation beside your project files rather than assuming that every plugin has already migrated.
The pre-migration baseline
Do not start by deleting Podfile, Podfile.lock, or custom Pod configuration. First create a release record that you can compare against the migrated build.
Record the following in a private migration note:
- The Flutter SDK version and the output of
flutter doctor -v. - The Xcode version selected by
xcode-select. - The contents and revision status of
pubspec.yamlandpubspec.lock. - The current
ios/Podfile,Podfile.lock, workspace files, local pods, private specifications, and manually added frameworks. - The signing method, team setting, bundle identifier, export method, and the last successful Archive.
- The build number, version number, archive location, and TestFlight status of the last accepted build.
- The native features that must be tested, such as notifications, camera access, payments, deep links, background tasks, or authentication.
Use a separate Git branch, disposable checkout, or isolated Mac. Never make the first migration directly on the only machine that produces production releases.
A useful baseline has a clear rollback point. If the migration fails, you should be able to restore the branch, select the previous Flutter SDK, run the known dependency commands, and produce the same release path without reconstructing signing settings from memory.
The minimum command record
Run the following as the normal build account, from the redacted project directory. Replace /Users/BUILD_USER/src/REDACTED_APP and /opt/flutter/stable with your real paths. Do not paste a real bundle identifier, team identifier, certificate name, or private log into a public issue.
# Account: BUILD_USER
# Project: /Users/BUILD_USER/src/REDACTED_APP
# Flutter SDK: /opt/flutter/stable
cd /Users/BUILD_USER/src/REDACTED_APP
/opt/flutter/stable/bin/flutter --version
/opt/flutter/stable/bin/flutter doctor -v
xcode-select -p
git status --short
Expected evidence includes the intended Flutter SDK path, the selected Xcode developer directory, and a clean or deliberately modified Git state. Save the output as a migration artifact. The undo action is not another command: restore the baseline branch or discard the isolated checkout before touching the production repository.
The first compatibility pass
A Flutter dependency list is not enough. You need to identify which packages add iOS native code and how each one enters the Xcode project.
Start with the dependency tree:
# Account: BUILD_USER
# Project: /Users/BUILD_USER/src/REDACTED_APP
cd /Users/BUILD_USER/src/REDACTED_APP
/opt/flutter/stable/bin/flutter pub deps --style=compact
/opt/flutter/stable/bin/flutter pub get
The expected result is a resolved Dart dependency graph and a current lock file. Save the terminal output and inspect the build log for plugin registration, package resolution, native compilation, and generated project changes.
Create a compatibility note with one entry per native plugin:
- Plugin name and locked revision.
- Whether it contains iOS native source.
- Whether its current documentation or package structure indicates SwiftPM support.
- Whether it has a custom Podfile hook, local pod, private specification, framework, or build script.
- Whether it pins a minimum iOS version or adds a transitive native dependency.
- The planned route: SwiftPM, CocoaPods fallback, replacement, or manual review.
- The evidence required before the plugin can enter a release Archive.
Do not classify a plugin as migrated merely because a Debug build succeeds. Debug compilation can miss Release-only linker settings, signing configuration, generated resource handling, or an upload-time issue.
The Flutter plugin development documentation is useful for identifying how a plugin exposes native platform code. Your project still remains the source of truth because private packages and custom scripts can behave differently from a public plugin example.
SwiftPM project changes
Flutter may update or generate iOS project content during dependency resolution. That is expected, but every change must be reviewable.
After creating the migration branch, run dependency resolution and inspect the diff:
# Account: BUILD_USER
# Project: /Users/BUILD_USER/src/REDACTED_APP
# Flutter SDK: /opt/flutter/stable
cd /Users/BUILD_USER/src/REDACTED_APP
git diff -- ios pubspec.lock
/opt/flutter/stable/bin/flutter pub get
git status --short
git diff -- ios pubspec.lock
The expected evidence is a small, explainable set of project and lock-file changes. The exact files vary with the project, plugin set, and generated state. Review new package references, workspace changes, package product links, deployment targets, and plugin registration.
If the diff includes an unexpected hand-written build script, a changed signing setting, or a removed native resource, stop and compare it with the baseline. The safest rollback is to reset the isolated branch to the recorded commit, not to edit generated files until the build appears green.
For the first compile, avoid signing:
# Account: BUILD_USER
# Project: /Users/BUILD_USER/src/REDACTED_APP
cd /Users/BUILD_USER/src/REDACTED_APP
/opt/flutter/stable/bin/flutter build ios --release --no-codesign
The expected output is completion of dependency resolution, plugin compilation, and an unsigned iOS build. It is not a release acceptance result. If the command fails, preserve the complete log, identify the first native error, and revert the migration branch before trying another dependency change.
Plugin fallback and mixed dependency resolution
When a plugin lacks SwiftPM support
Keep that plugin on CocoaPods or replace it only after checking its current compatibility evidence. Do not modify third-party plugin source solely to make the project look SwiftPM-only. Such a patch creates a maintenance fork and can hide the real issue until the next plugin update.
A fallback is reasonable when:
- The plugin has native iOS code but no documented SwiftPM integration.
- Its package still requires a Podfile hook or a private pod specification.
- SwiftPM resolution fails on a transitive dependency.
- A replacement would change a critical native feature shortly before release.
Document the fallback and set a review condition, such as the next planned dependency maintenance window or the plugin's confirmed SwiftPM release. Do not invent a migration date for a plugin whose compatibility is unverified.
Can SwiftPM and CocoaPods coexist?
Yes, they can carry different dependencies during the transition. They must not introduce the same native product twice.
Use the failure evidence to choose the next action:
- Duplicate symbols or duplicate framework links: identify the library supplied by both managers, then remove one route.
- Module not found: check package product linkage, plugin registration, and the build configuration that failed.
- Minimum system version conflict: compare the package requirement with the deployment target and the plugin's documented constraint.
- Remote package download failure: verify network access, package revision availability, and the clean-environment behavior before blaming signing.
- Release-only linker failure: compare Release settings with Debug settings and inspect the Archive log.
A mixed project is not automatically unstable. An undocumented mixed project is. Keep a dependency manifest that names the owner of each native library and records why the project still uses CocoaPods for it.
Do not use a successful simulator build as proof that the dependency chain is ready for distribution. Archive, export, upload, and installation exercise different parts of the release path.
Remote Mac and reproducible builds
If you develop on Windows or Linux, use the remote Mac as a controlled build environment rather than as a place to make undocumented fixes. Your local machine can edit Dart code and review Git changes, but iOS dependency resolution, Xcode compilation, signing, Archive, and TestFlight upload must be verified on the Mac that will perform them.
Fix these inputs before comparing builds:
- Flutter SDK path and revision.
- Xcode selection and command-line tools.
- The project checkout and Git revision.
- Dependency lock files.
- Build account and environment variables.
- Keychain access and signing permissions.
- Network access for package resolution.
- The recovery procedure after a disconnected SSH or VNC session.
Keep SwiftPM caches separate from signing material. A cache can speed up a later build, but it is not evidence that a clean machine can resolve the project. Perform a clean-environment test with caches unavailable or cleared, then record whether the same package revisions resolve.
Signing certificates, private keys, provisioning profiles, and App Store Connect credentials should not be stored in dependency-cache folders. Limit their access to the build account, document the keychain state, and record how you will reconnect after a remote session interruption.
For teams using a remote Mac regularly, the SSHMac Mac environment page can help you evaluate whether a separate build machine fits this workflow. The decision should be based on reproducibility and access control, not simply on whether an interactive Xcode window opens.
Archive and TestFlight acceptance
A successful unsigned build is only the midpoint. The new dependency chain must pass the same release process as the old chain.
Run the checks in this order:
- Confirm the version and build number against the release note.
- Select the intended Xcode developer directory.
- Open the generated workspace or project specified by the migrated build.
- Execute a Release Archive with the production scheme.
- Inspect the archive for the expected application bundle, embedded frameworks, resources, and signing state.
- Export the IPA using the intended distribution method.
- Upload the build using the documented App Store Connect route.
- Wait for processing and record the resulting build status.
- Install the build through TestFlight.
- Exercise every feature backed by a native plugin.
- Compare the result with the last accepted build.
The Flutter iOS deployment guide covers the supported release path. For upload behavior, follow Apple's build upload rules and check the App Store build status definitions. A successful upload is not the same as an installable TestFlight build.
Keep the old dependency chain available until the new build has passed real TestFlight installation and native feature verification. If Archive fails after SwiftPM migration, restore the previous branch or commit, restore the previous lock files, select the known Flutter SDK, and rerun the baseline commands. Do not repair the production branch by manually deleting random package references.
The migration decision branches
Use these conditions after the first complete acceptance cycle:
- If every native plugin resolves through SwiftPM, the unsigned build succeeds, Archive succeeds, and TestFlight features pass: switch the production branch to the SwiftPM path after a reviewed merge.
- If only some plugins support SwiftPM and the remaining plugins are stable under CocoaPods: keep the dual-track setup and document ownership, versions, and duplicate-library checks.
- If a critical plugin fails SwiftPM resolution but works through CocoaPods: retain CocoaPods for that plugin and schedule a later review; do not force a source patch before a release.
- If the remote Mac can reproduce the old and new builds but the production Mac cannot: keep production unchanged and investigate toolchain drift.
- If Archive or TestFlight fails and the rollback build is available: revert, publish through the known path, and treat the migration as incomplete.
- If the project has no isolated Mac and no reliable rollback record: delay the migration until both are available.
Score the migration by evidence, not by the number of files changed:
- Green: clean resolution, Release Archive, upload, processing, TestFlight install, and native feature checks all pass.
- Amber: the build passes but a plugin remains on CocoaPods or a clean-environment test is pending.
- Red: duplicate native products, unresolved modules, signing drift, or a failed rollback remains unexplained.
Current Mac versus a separate remote Mac
Using the only production Mac for this migration has three real weaknesses: one dependency experiment can block the release path, an interrupted session can leave the workspace in an unclear state, and the machine may contain undocumented caches or signing changes that make the result impossible to reproduce. Developing on Windows or Linux adds another limitation because the final Xcode, Archive, signing, and TestFlight checks still require macOS.
A separate Mac does not remove the need for disciplined dependency records, and it is not automatically the best choice for a permanent high-volume build farm. It is most useful when you need a temporary migration environment, a second release path, or a stable Mac while the main machine remains untouched. In that situation, renting a Mac from SSHMac gives you a practical place to run the Flutter 3.47 dual-track test, preserve the old production chain, and decide later whether the remote setup should remain part of your workflow.
If you need a temporary iOS build environment, start with the smallest access period that covers dependency resolution, Archive, upload, and TestFlight verification. Once both dependency paths have passed acceptance, you can make an informed choice between keeping the rented Mac, maintaining your own hardware, or returning to a single local build machine.