Adding Swift Package Manager Support to a Flutter Plugin Without Breaking CocoaPods
Welcome to follow the WeChat official account: FSA Full Stack Action 👋
1. Background
The chat_bottom_container plugin has been published for a while. The iOS side had only been configured with CocoaPods, using a scheme of "pre-compiled xcframework + prepare_command in podspec to download from GitHub Releases". Until recently, someone raised an issue requesting support for Swift Package Manager (hereinafter referred to as SPM): chat_bottom_container#39
After understanding it, I found that from Flutter 3.44 onwards, SPM is enabled by default. My plugin only had a podspec and no Package.swift, so a pure SPM project naturally couldn't find it. The default behavior before and after 3.44 is exactly reversed:
| Flutter Version | SPM Default Status | Manual Switch |
|---|---|---|
| < 3.44 | Off (opt-in) | Must manually execute flutter config --enable-swift-package-manager to turn on |
| 3.44+ | Enabled by default | Conversely, to turn it off, set enable-swift-package-manager: false (see end of article for details) |
By the way, the official CocoaPods repository will become read-only and enter maintenance mode starting from 2026-12-02. SPM is the clear future, and plugins will have to add support sooner or later.
So, let's add it. But before starting, compatibility needs to be considered, so that developers using the new version can smoothly use SPM, and developers using CocoaPods are not affected. Below is the entire process from start to finish, along with a few points I stepped into along the way. The final changes are all in this PR: #40.
2. Implementation
1. Should podspec be deleted?
When adding SPM, should the podspec be replaced? The conclusion is no, it cannot be deleted. Keeping it is the officially recommended practice. There are several reasons:
- Not everyone is on
3.44+. Before3.44,SPMhad to be manually enabled withflutter config --enable-swift-package-manager; even if upgraded to3.44+where it's on by default, users can actively turn it off (enable-swift-package-manager: false). For these projects still usingCocoaPods, deleting thepodspecwould directly cause a build failure. Flutteritself has an "either-or fallback" mechanism: if the project hasSPMenabled and the plugin has aPackage.swift, it usesSPM; otherwise, it honestly falls back topodspec. With both files present, the tool chooses automatically, and they don't conflict at all.- Looking at official plugins (
path_provider_foundation,shared_preferences_foundation, etc.), they all have bothPackage.swiftandpodspeccoexisting.
So the idea is very clear: Adding SPM means "adding" a Package.swift, letting it and the podspec compile the same source code and depend on the same xcframework, rather than choosing one over the other. Unless completely abandoning CocoaPods, it shouldn't be deleted; otherwise, for an already published public plugin, that's a real breaking change.
2. Moving Source Code Location
SPM has strict requirements for directory structure. The source code for a target must be placed under Sources/<target name>/ by default. The original Swift files were in ios/Classes/, which doesn't match the location, so move them first:
ios/
├── chat_bottom_container/
│ ├── Package.swift 👈 Newly added
│ └── Sources/
│ └── chat_bottom_container/
│ ├── ChatBottomContainerPlugin.swift 👈 Moved from Classes/
│ └── FSAChatBottomContainerGeneratedApis.g.swift 👈 pigeon generated file
├── Frameworks/
│ └── FSAChatBottomContainer.xcframework
└── chat_bottom_container.podspec
Once the source code is moved, the source_files in podspec still points to the old address. If not changed, the CocoaPods side won't find the files. Update it to the new location:
# Before
- s.source_files = 'Classes/**/*'
# After
+ s.source_files = 'chat_bottom_container/Sources/chat_bottom_container/**/*.swift'
3. pigeon's swiftOut
The FSAChatBottomContainerGeneratedApis.g.swift above is generated by pigeon, and its output path is hardcoded in pigeons/bottom_container.dart. After the file is moved, if pigeon is re-run someday, it will generate a file at the old path again, creating an orphan file. This kind of landmine won't explode now, but it will when you've forgotten about it, so fix it together:
// Before
- swiftOut: 'ios/Classes/FSAChatBottomContainerGeneratedApis.g.swift',
// After
+ swiftOut: 'ios/chat_bottom_container/Sources/chat_bottom_container/FSAChatBottomContainerGeneratedApis.g.swift',
4. Writing Package.swift
This step is the core. Package.swift looks like this:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "chat_bottom_container",
platforms: [
.iOS("13.0"),
],
products: [
.library(name: "chat-bottom-container", targets: ["chat_bottom_container"]),
],
dependencies: [
// Provided by the Flutter tool at build time (Flutter 3.44+),
// allowing the plugin target to access the `Flutter` module.
.package(name: "FlutterFramework", path: "../FlutterFramework"),
],
targets: [
// Pre-compiled native implementation, hosted on GitHub Releases.
// Consistent with the vendored_frameworks in podspec,
// ensuring the SPM and CocoaPods build paths always use the same source.
.binaryTarget(
name: "FSAChatBottomContainer",
url: "https://github.com/LinXunFeng/flutter_chat_packages_pub/releases/download/chat_bottom_container/ios_0.0.1.zip",
checksum: "b9c380b72010e5d378cc813fbc5cca9b21471b82134e633567a6beee4b5a2a91"
),
.target(
name: "chat_bottom_container",
dependencies: [
"FSAChatBottomContainer",
.product(name: "FlutterFramework", package: "FlutterFramework"),
]
),
]
)
There are three points here that need emphasis.
First, the pre-compiled artifact uses binaryTarget to reference a remote zip. The CocoaPods side has always relied on podspec's prepare_command to use curl to download the xcframework from GitHub Releases. There's no need to set up a separate system for SPM; just point binaryTarget to the same zip from the same Release. After unzipping this zip, the root directory is FSAChatBottomContainer.xcframework, which perfectly meets the requirements of binaryTarget. No binary needs to be stuffed into the repository.
Second, the checksum is not optional. SPM requires a checksum when referencing a remote binary, otherwise it will directly report an error. This value is calculated using this command:
swift package compute-checksum ios_0.0.1.zip
Third, where does the import Flutter module in the plugin code come from in the SPM scenario? It is obtained through the dependency .package(name: "FlutterFramework", path: "../FlutterFramework"). FlutterFramework is dynamically injected into the project by the Flutter tool at build time. There's a pitfall hidden here: This dependency only exists in Flutter 3.44+. The minimum version limit in the README later is essentially dictated by this.
5. Finishing Touches: Version Alignment and Ignoring Artifacts
The original minimum system version in podspec was 11.0. The SPM / Flutter setup requires a higher one, so both sides are uniformly raised to 13.0:
s.platform = :ios, '13.0' # podspec
platforms: [ .iOS("13.0") ], // Package.swift
Also, SPM generates a bunch of local caches and lock files upon building, which shouldn't be committed. Add three lines to ios/.gitignore to ignore them:
.build/
.swiftpm/
Package.resolved
3. Verification
The trickiest part of this change is: Flutter's fallback mechanism can make you think you're testing SPM, when it's actually silently using CocoaPods, leading you to believe the new feature works. So the verification standard isn't "can it compile", but confirming which path it actually took. The goal is to get the same example running in both modes separately, each taking its own path without crossover.
1. CocoaPods Mode: First, ensure no pitfalls for existing users
The highest risk here is for existing users. The source code was moved, and the source_files path was changed. A slight mistake could break the CocoaPods path. First, turn off SPM and run it:
cd packages/chat_bottom_container/example
flutter config --no-enable-swift-package-manager # Ensure SPM is off
flutter clean
cd ios && rm -rf Pods Podfile.lock && pod install && cd ..
flutter build ios --simulator --debug
To judge that it indeed used CocoaPods, check these points:
chat_bottom_containercan be found inexample/ios/Podfile.lock- The pod's directory exists under
example/ios/Pods/ Frameworks/FSAChatBottomContainer.xcframeworkwas downloaded byprepare_command- It can successfully compile the Swift source code I moved, indicating the new path in
source_filesis correct
2. SPM Mode: Then prove the new feature actually works
Turn SPM back on and do it again:
cd packages/chat_bottom_container/example
flutter config --enable-swift-package-manager # Enable SPM
flutter clean
cd ios && rm -rf Pods Podfile.lock && cd ..
flutter build ios --simulator --debug
This time, check the following points to confirm it really used SPM and didn't silently fall back:
Fluttergenerated an aggregate package. Openexample/ios/Flutter/ephemeral/.../FlutterGeneratedPluginSwiftPackage/Package.swift, and there's a dependency.package(name: "chat_bottom_container", path: ...)inside.chat_bottom_containeris no longer found inPodfile.lock(it has been moved out ofPods, leaving onlyFlutteritself).- The build log prints
Downloading binary artifact ...ios_0.0.1.zip, indicatingbinaryTargettook effect. - To confirm where it was downloaded, you can look in Xcode's
DerivedData.SPMis driven by Xcode, and the downloadedzipis extracted to theRunnerproject'sSourcePackages/artifacts/directory. The path is roughly:
~/Library/Developer/Xcode/DerivedData/Runner-<random string>/SourcePackages/artifacts/chat_bottom_container/FSAChatBottomContainer/
# Inside is the extracted FSAChatBottomContainer.xcframework
This is very different from
CocoaPods:CocoaPodsusesprepare_commandto download thexcframeworkinto the plugin repository'sios/Frameworks/;SPMputs it into Xcode'sDerivedDatacache, not into your project directory.
The easiest judgment method: Just check if this plugin is in Podfile.lock. If it is, CocoaPods is managing it; if not, but the app still works normally, then SPM is managing it.
4. Conclusion
Version Compatibility Boundaries
The thing most likely to trip up users is version compatibility, which is also specifically written in the README. The reason lies in the FlutterFramework dependency mentioned earlier, which only exists in Flutter 3.44+. The compatibility situation is summarized in a table:
| Integration Method | Flutter Version Requirement | Description |
|---|---|---|
| CocoaPods | No special requirement | Works out of the box, behavior is exactly the same as before |
| Swift Package Manager | 3.44 and above | SPM is enabled by default from 3.44 onwards |
| Swift Package Manager | 3.24–3.43 | Although SPM can be manually enabled, FlutterFramework is missing. Please continue using CocoaPods |
So existing users don't need to worry: as long as they don't upgrade Flutter and continue using CocoaPods, this change has no impact on them.
Appendix: How to turn off SPM on 3.44
If you've upgraded to 3.44+ but temporarily want to stick with CocoaPods (for example, a dependency hasn't adapted to SPM yet), you can actively turn it off at two levels.
To turn it off for only a single project, add a config: section under flutter: in the app's pubspec.yaml. This will follow the repository and take effect for all collaborators:
flutter:
config:
enable-swift-package-manager: false
To turn it off globally for all projects of the current user, use the command line (which is the command run in the "Verification" section for CocoaPods mode):
flutter config --no-enable-swift-package-manager
Note that simply "turning it off" only stops using SPM. The SPM integration previously written into the Xcode project by flutter run remains. To completely clean it out, you must first disable it, then flutter clean, then go into Xcode and delete FlutterGeneratedPluginSwiftPackage from Package Dependencies and Frameworks, Libraries, and Embedded Content, and finally go to Product > Scheme > Edit Scheme → Build → Pre-actions and delete the Run Prepare Flutter Framework Script.
Both Paths Must Use the Same Source
The source code, binaries, and minimum version referenced by SPM and CocoaPods must be completely identical. If each side maintains its own set, a version upgrade will cause the problem of "people using CocoaPods and people using SPM getting two different implementations", which is very troublesome to debug. So source code all points to Sources/, binaries are all locked to the same Release, and the minimum version is all set to 13.0.
This also brings out a pitfall during release: in the future, when the xcframework is updated, the version/URL in podspec and the url + checksum in Package.swift must be changed together. Missing one will cause the two sides to be out of sync. I wrote a comment in Package.swift as a reminder for this.
Checklist for Supporting SPM
If you also have a plugin that needs to support SPM, follow these steps:
- Move source code to the
Sources/<target>/layout, and updatesource_filesinpodspecaccordingly; - Synchronously update the output paths of code generation tools like
pigeon, don't leave orphan files; - Add a new
Package.swift, usebinaryTargetfor pre-compiled artifacts pointing to the sameRelease, and calculatechecksumwithswift package compute-checksum; - Obtain the
Fluttermodule viaFlutterFramework, and set the minimum version toFlutter 3.44+based on this; - Align the minimum system version on both sides (I set it to
13.0this time); - Ignore
.build/,.swiftpm/,Package.resolvedin.gitignore; - Keep the
podspec, and clearly state the version compatibility boundaries in theREADME; - Verify both
CocoaPods/SPMmodes separately, usingPodfile.lockto judge which path was actually taken.
Related Links:
- Repository: flutter_chat_packages
- Issue: #39
- PR: #40
- Official Documentation: Swift Package Manager for app developers
That's all for this record. I hope it helps those who are also adding SPM support.
If this article was helpful to you, please don't hesitate to click and follow my WeChat official account: FSA Full Stack Action, this will be the greatest encouragement for me. The official account has not only
iOStechnology, but also articles onAndroid,Flutter,Python, etc. There might be skill points you want to learn about!