Swift Package Manager for plugin authors
How to add Swift Package Manager compatibility to iOS and macOS plugins
For information on how to turn SwiftPM off and on, visit Swift Package Manager for app developers.
How to add Swift Package Manager support to an existing Flutter plugin
#This guide shows how to add Swift Package Manager support to a plugin that already supports CocoaPods. This ensures the plugin is usable by all Flutter projects.
Flutter plugins should support both Swift Package Manager and CocoaPods until further notice.
Swift Package Manager adoption will be gradual. As of Flutter 3.44, plugins that don't support CocoaPods aren't usable by projects that haven't migrated to Swift Package Manager yet. Plugins that don't support Swift Package Manager can cause problems for projects that have migrated. Please migrate your plugins as soon as possible.
Replace plugin_name throughout this guide with the name of your plugin.
The example below uses ios, replace ios with macos
or darwin, as applicable.
Ensure that you are using Flutter 3.44 or later. This enables SwiftPM by default.
-
Start by creating a directory under the
ios,macos, and/ordarwindirectories. Name this new directory the name of the platform package.plugin_name/
ios/
- …
- plugin_name/
-
Within this new directory, create the following files/directories:
Package.swift(file)Sources(directory)Sources/plugin_name(directory)
Your plugin should look like:
plugin_name/
ios/
- …
plugin_name/
- Package.swift
Sources/
- plugin_name/
-
Use the following template in the
Package.swiftfile:Package.swiftswift// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( // TODO: Update your plugin name. name: "plugin_name", platforms: [ // TODO: Update the platforms your plugin supports. // If your plugin only supports iOS, remove `.macOS(...)`. // If your plugin only supports macOS, remove `.iOS(...)`. .iOS("13.0"), .macOS("10.15") ], products: [ // TODO: Update your library and target names. // If the plugin name contains "_", replace with "-" for the library name. .library(name: "plugin-name", targets: ["plugin_name"]) ], dependencies: [ .package(name: "FlutterFramework", path: "../FlutterFramework") ], targets: [ .target( // TODO: Update your target name. name: "plugin_name", dependencies: [ .product(name: "FlutterFramework", package: "FlutterFramework") ], resources: [ // TODO: If your plugin requires a privacy manifest // (e.g. if it uses any required reason APIs), update the PrivacyInfo.xcprivacy file // to describe your plugin's privacy impact, and then uncomment this line. // For more information, visit: // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files // .process("PrivacyInfo.xcprivacy"), // TODO: If you have other resources that need to be bundled with your plugin, refer to // the following instructions to add them: // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package ] ) ] ) -
Update the supported platforms in your
Package.swiftfile.Package.swiftswiftplatforms: [ // TODO: Update the platforms your plugin supports. // If your plugin only supports iOS, remove `.macOS(...)`. // If your plugin only supports macOS, remove `.iOS(...)`. .iOS("13.0"), .macOS("10.15") ], -
Update the package, library, and target names in your
Package.swiftfile.Package.swiftswiftlet package = Package( // TODO: Update your plugin name. name: "plugin_name", platforms: [ .iOS("13.0"), .macOS("10.15") ], products: [ // TODO: Update your library and target names. // If the plugin name contains "_", replace with "-" for the library name .library(name: "plugin-name", targets: ["plugin_name"]) ], dependencies: [], targets: [ .target( // TODO: Update your target name. name: "plugin_name", dependencies: [], resources: [ // TODO: If your plugin requires a privacy manifest // (e.g. if it uses any required reason APIs), update the PrivacyInfo.xcprivacy file // to describe your plugin's privacy impact, and then uncomment this line. // For more information, visit: // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files // .process("PrivacyInfo.xcprivacy"), // TODO: If you have other resources that need to be bundled with your plugin, refer to // the following instructions to add them: // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package ] ) ] ) -
If your plugin has a
PrivacyInfo.xcprivacyfile, move it toios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacyand uncomment the resource in thePackage.swiftfile.Package.swiftswiftresources: [ // TODO: If your plugin requires a privacy manifest // (e.g. if it uses any required reason APIs), update the PrivacyInfo.xcprivacy file // to describe your plugin's privacy impact, and then uncomment this line. // For more information, visit: // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files .process("PrivacyInfo.xcprivacy"), // TODO: If you have other resources that need to be bundled with your plugin, refer to // the following instructions to add them: // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package ], -
Move any resource files from
ios/Assetstoios/plugin_name/Sources/plugin_name(or a subdirectory). Add the resource files to yourPackage.swiftfile, if applicable. For more instructions, visit Bundling resources with a Swift package.
-
Move all files from
ios/Classestoios/plugin_name/Sources/plugin_name. -
Add the
FlutterFrameworkas a dependency and update Dart and Flutter versions.Update
Package.swiftto includeFlutterFramework:Package.swiftswiftdependencies: [ .package(name: "FlutterFramework", path: "../FlutterFramework") ], targets: [ .target( // TODO: Update your target name. name: "plugin_name", dependencies: [ .product(name: "FlutterFramework", package: "FlutterFramework") ],In
pubspec.yaml, update versions to:pubspec.yamlyamlenvironment: sdk: ^3.11.0 flutter: ">=3.41.0" -
The
ios/Assets,ios/Resources, andios/Classesdirectories should now be empty and can be deleted. -
If your plugin uses Pigeon, update your Pigeon input file.
pigeons/messages.dartdartkotlinOptions: KotlinOptions(), javaOut: 'android/app/src/main/java/io/flutter/plugins/Messages.java', javaOptions: JavaOptions(), swiftOut: 'ios/Classes/messages.g.swift', swiftOut: 'ios/plugin_name/Sources/plugin_name/messages.g.swift', swiftOptions: SwiftOptions(), -
Update your
Package.swiftfile with any customizations you might need.In Xcode, open the
ios/plugin_name/directory.In Xcode, open your
Package.swiftfile. Verify Xcode doesn't produce any warnings or errors for this file.If your
ios/plugin_name.podspecfile has CocoaPodsdependencys, add the corresponding Swift Package Manager dependencies to yourPackage.swiftfile.If your package must be linked explicitly
staticordynamic(not recommended by Apple), update the Product to define the type:Package.swiftswiftproducts: [ .library(name: "plugin-name", type: .static, targets: ["plugin_name"]) ],Make any other customizations. For more information on how to write a
Package.swiftfile, visitPackageDescription.
-
Update your
ios/plugin_name.podspecto point to new paths.ios/plugin_name.podspecrubys.source_files = 'Classes/**/*.swift' s.resource_bundles = {'plugin_name_privacy' => ['Resources/PrivacyInfo.xcprivacy']} s.source_files = 'plugin_name/Sources/plugin_name/**/*.swift' s.resource_bundles = {'plugin_name_privacy' => ['plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy']} -
Update loading of resources from bundle to use
Bundle.module.swift#if SWIFT_PACKAGE let settingsURL = Bundle.module.url(forResource: "image", withExtension: "jpg") #else let settingsURL = Bundle(for: Self.self).url(forResource: "image", withExtension: "jpg") #endif -
If your
.gitignoredoesn't include.build/and.swiftpm/directories, you'll want to update your.gitignoreto include:.gitignore.build/ .swiftpm/Commit your plugin's changes to your version control system.
-
Verify the plugin still works with CocoaPods.
Turn off Swift Package Manager.
shflutter config --no-enable-swift-package-managerNavigate to the plugin's example app.
shcd path/to/plugin/example/Ensure the plugin's example app builds and runs.
shflutter runNavigate to the plugin's top-level directory.
shcd path/to/plugin/Run CocoaPods validation lints.
shpod lib lint ios/plugin_name.podspec --configuration=Debug --skip-tests --use-modular-headers --use-librariesshpod lib lint ios/plugin_name.podspec --configuration=Debug --skip-tests --use-modular-headers
-
Verify the plugin works with Swift Package Manager.
Turn on Swift Package Manager.
shflutter config --enable-swift-package-managerNavigate to the plugin's example app.
shcd path/to/plugin/example/Ensure the plugin's example app builds and runs.
shflutter runIn Xcode, open the plugin's example app. Ensure that Package Dependencies shows in the left Project Navigator.
-
Verify tests pass.
If your plugin has native unit tests (XCTest), make sure you also update unit tests in the plugin's example app.
Follow instructions for testing plugins.
Replace plugin_name throughout this guide with the name of your plugin.
The example below uses ios, replace ios with macos
or darwin, as applicable.
Ensure that you are running Flutter 3.44 or later. This enables SwiftPM by default.
-
Start by creating a directory under the
ios,macos, and/ordarwindirectories. Name this new directory the name of the platform package.plugin_name/
ios/
- …
- plugin_name/
-
Within this new directory, create the following files/directories:
Package.swift(file)Sources(directory)Sources/plugin_name(directory)Sources/plugin_name/include(directory)Sources/plugin_name/include/plugin_name(directory)Sources/plugin_name/include/plugin_name/.gitkeep(file)- This file ensures the directory is committed.
You can remove the
.gitkeepfile if other files are added to the directory.
- This file ensures the directory is committed.
You can remove the
Your plugin should look like:
plugin_name/
ios/
- …
plugin_name/
- Package.swift
Sources/plugin_name/include/plugin_name/
- .gitkeep/
-
Use the following template in the
Package.swiftfile:Package.swiftswift// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( // TODO: Update your plugin name. name: "plugin_name", platforms: [ // TODO: Update the platforms your plugin supports. // If your plugin only supports iOS, remove `.macOS(...)`. // If your plugin only supports macOS, remove `.iOS(...)`. .iOS("13.0"), .macOS("10.15") ], products: [ // TODO: Update your library and target names. // If the plugin name contains "_", replace with "-" for the library name .library(name: "plugin-name", targets: ["plugin_name"]) ], dependencies: [], targets: [ .target( // TODO: Update your target name. name: "plugin_name", dependencies: [], resources: [ // TODO: If your plugin requires a privacy manifest // (in other words, if it uses any required reason APIs), // update the PrivacyInfo.xcprivacy file // to describe your plugin's privacy impact, and then uncomment this line. // For more information, visit: // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files // .process("PrivacyInfo.xcprivacy"), // TODO: If you have other resources that need to be bundled with your plugin, refer to // the following instructions to add them: // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package ], cSettings: [ // TODO: Update your plugin name. .headerSearchPath("include/plugin_name") ] ) ] ) -
Update the supported platforms in your
Package.swiftfile.Package.swiftswiftplatforms: [ // TODO: Update the platforms your plugin supports. // If your plugin only supports iOS, remove `.macOS(...)`. // If your plugin only supports macOS, remove `.iOS(...)`. .iOS("13.0"), .macOS("10.15") ], -
Update the package, library, and target names in your
Package.swiftfile.Package.swiftswiftlet package = Package( // TODO: Update your plugin name. name: "plugin_name", platforms: [ .iOS("13.0"), .macOS("10.15") ], products: [ // TODO: Update your library and target names. // If the plugin name contains "_", replace with "-" for the library name .library(name: "plugin-name", targets: ["plugin_name"]) ], dependencies: [], targets: [ .target( // TODO: Update your target name. name: "plugin_name", dependencies: [], resources: [ // TODO: If your plugin requires a privacy manifest // (for example, if it uses any required reason APIs), // update the PrivacyInfo.xcprivacy file // to describe your plugin's privacy impact, and then uncomment this line. // For more information, visit: // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files // .process("PrivacyInfo.xcprivacy"), // TODO: If you have other resources that need to be bundled with your plugin, refer to // the following instructions to add them: // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package ], cSettings: [ // TODO: Update your plugin name. .headerSearchPath("include/plugin_name") ] ) ] ) -
If your plugin has a
PrivacyInfo.xcprivacyfile, move it toios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacyand uncomment the resource in thePackage.swiftfile.Package.swiftswiftresources: [ // TODO: If your plugin requires a privacy manifest // (for example, if it uses any required reason APIs), // update the PrivacyInfo.xcprivacy file // to describe your plugin's privacy impact, and then uncomment this line. // For more information, visit: // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files .process("PrivacyInfo.xcprivacy"), // TODO: If you have other resources that need to be bundled with your plugin, refer to // the following instructions to add them: // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package ], -
Move any resource files from
ios/Assetstoios/plugin_name/Sources/plugin_name(or a subdirectory). Add the resource files to yourPackage.swiftfile, if applicable. For more instructions, visit https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package. -
Move any public headers from
ios/Classestoios/plugin_name/Sources/plugin_name/include/plugin_name.If you're unsure which headers are public, check your
podspecfile'spublic_header_filesattribute. If this attribute isn't specified, all of your headers were public. You should consider whether you want all of your headers to be public.The
pluginClassdefined in yourpubspec.yamlfile must be public and within this directory.
-
Handling
modulemap.Skip this step if your plugin does not have a
modulemap.If you're using a
modulemapfor CocoaPods to create a Test submodule, consider removing it for Swift Package Manager. Note that this makes all public headers available through the module.To remove the
modulemapfor Swift Package Manager but keep it for CocoaPods, exclude themodulemapand umbrella header in the plugin'sPackage.swiftfile.The example below assumes the
modulemapand umbrella header are located in theios/plugin_name/Sources/plugin_name/includedirectory.Package.swiftswift.target( name: "plugin_name", dependencies: [], exclude: ["include/cocoapods_plugin_name.modulemap", "include/plugin_name-umbrella.h"],If you want to keep your unit tests compatible with both CocoaPods and Swift Package Manager, you can try the following:
Tests/TestFile.mobjc@import plugin_name; @import plugin_name.Test; #if __has_include(<plugin_name/plugin_name-umbrella.h>) @import plugin_name.Test; #endifIf you would like to use a custom
modulemapwith your Swift package, refer to Swift Package Manager's documentation. -
Move all remaining files from
ios/Classestoios/plugin_name/Sources/plugin_name. -
The
ios/Assets,ios/Resources, andios/Classesdirectories should now be empty and can be deleted. -
If your header files are no longer in the same directory as your implementation files, you should update your import statements.
For example, imagine the following migration:
Before:
ios/Classes/ ├── PublicHeaderFile.h └── ImplementationFile.mAfter:
ios/plugin_name/Sources/plugin_name/ └── include/plugin_name/ └── PublicHeaderFile.h └── ImplementationFile.m
In this example, the import statements in
ImplementationFile.mshould be updated:Sources/plugin_name/ImplementationFile.mobjc#import "PublicHeaderFile.h" #import "./include/plugin_name/PublicHeaderFile.h" -
If your plugin uses Pigeon, update your Pigeon input file.
pigeons/messages.dartdartjavaOptions: JavaOptions(), objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcHeaderOut: 'ios/plugin_name/Sources/plugin_name/messages.g.h', objcSourceOut: 'ios/plugin_name/Sources/plugin_name/messages.g.m', copyrightHeader: 'pigeons/copyright.txt',If your
objcHeaderOutfile is no longer within the same directory as theobjcSourceOut, you can change the#importusingObjcOptions.headerIncludePath:pigeons/messages.dartdartjavaOptions: JavaOptions(), objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcHeaderOut: 'ios/plugin_name/Sources/plugin_name/include/plugin_name/messages.g.h', objcSourceOut: 'ios/plugin_name/Sources/plugin_name/messages.g.m', objcOptions: ObjcOptions( headerIncludePath: './include/plugin_name/messages.g.h', ), copyrightHeader: 'pigeons/copyright.txt',Run Pigeon to re-generate its code with the latest configuration.
-
Update your
Package.swiftfile with any customizations you might need.In Xcode, open the
ios/plugin_name/directory.In Xcode, open your
Package.swiftfile. Verify that Xcode doesn't produce any warnings or errors for this file.If your
ios/plugin_name.podspecfile has CocoaPodsdependencys, add the corresponding Swift Package Manager dependencies to yourPackage.swiftfile.If your package must be linked explicitly
staticordynamic(not recommended by Apple), update the Product to define the type:Package.swiftswiftproducts: [ .library(name: "plugin-name", type: .static, targets: ["plugin_name"]) ],Make any other customizations. For more information on how to write a
Package.swiftfile, visit https://developer.apple.com/documentation/packagedescription.
-
Update your
ios/plugin_name.podspecto point to new paths.ios/plugin_name.podspecrubys.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/cocoapods_plugin_name.modulemap' s.resource_bundles = {'plugin_name_privacy' => ['Resources/PrivacyInfo.xcprivacy']} s.source_files = 'plugin_name/Sources/plugin_name/**/*.{h,m}' s.public_header_files = 'plugin_name/Sources/plugin_name/include/**/*.h' s.module_map = 'plugin_name/Sources/plugin_name/include/cocoapods_plugin_name.modulemap' s.resource_bundles = {'plugin_name_privacy' => ['plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy']} -
Update loading of resources from bundle to use
SWIFTPM_MODULE_BUNDLE:objc#if SWIFT_PACKAGE NSBundle *bundle = SWIFTPM_MODULE_BUNDLE; #else NSBundle *bundle = [NSBundle bundleForClass:[self class]]; #endif NSURL *imageURL = [bundle URLForResource:@"image" withExtension:@"jpg"]; -
If your
ios/plugin_name/Sources/plugin_name/includedirectory only contains a.gitkeep, you'll want to update your.gitignoreto include the following:.gitignore!.gitkeepRun
flutter pub publish --dry-runto ensure theincludedirectory is published. Commit your plugin's changes to your version control system.
-
Verify the plugin still works with CocoaPods.
Turn off Swift Package Manager:
shflutter config --no-enable-swift-package-managerNavigate to the plugin's example app.
shcd path/to/plugin/example/Ensure the plugin's example app builds and runs.
shflutter runNavigate to the plugin's top-level directory.
shcd path/to/plugin/Run CocoaPods validation lints:
shpod lib lint ios/plugin_name.podspec --configuration=Debug --skip-tests --use-modular-headers --use-librariesshpod lib lint ios/plugin_name.podspec --configuration=Debug --skip-tests --use-modular-headers
-
Verify the plugin works with Swift Package Manager.
Turn on Swift Package Manager:
shflutter config --enable-swift-package-managerNavigate to the plugin's example app.
shcd path/to/plugin/example/Ensure the plugin's example app builds and runs.
shflutter runIn Xcode, open the plugin's example app. Ensure that Package Dependencies shows in the left Project Navigator.
-
Verify tests pass.
If your plugin has native unit tests (XCTest), make sure you also update unit tests in the plugin's example app.
Follow instructions for testing plugins.
(Optional, but recommended) Add plugin as local package in example app
#If your plugin includes an example, it is recommended to add the plugin as a local package in the example app. This is not required, but provides better Xcode support when editing the plugin's source code in the example app. Visit issue #179032.
Add plugin as local package
#In a terminal navigate to
my_plugin.-
In Xcode, run the following command to open the example app's workspace, (replace
ioswithmacosas appropriate):
open example/ios/Runner.xcworkspace
-
Right click Flutter > Add Files to “Runner”.

-
Select
my_plugin/ios/my_plugin(ormacosordarwin, as appropriate). -
Make sure “Reference files in place” is selected (it should be the default), and click Finish.

This adds the plugin as a local package, but it's referenced by absolute path, which isn't desirable for distribution. To change it to a relative path, use the following instructions.
Change to relative path
#-
Copy “Full Path” for plugin from the File Inspector.

-
In terminal:
open -a Xcode example/ios/Runner.xcodeproj/project.pbxproj -
Find the following:
path = [COPIED FULL PATH]; sourceTree = "<absolute>"For example:
path = /Users/username/path/to/my_plugin/ios/my_plugin; sourceTree = "<absolute>" -
And replace with relative path:
path = ../../ios/my_plugin; sourceTree = "<group>"(Adjust
iostomacosordarwinas needed).
How to update unit tests in a plugin's example app
#If your plugin has native XCTests, you might need to update them to work with Swift Package Manager if one of the following is true:
- You're using a CocoaPod dependency for the test.
- Your plugin is explicitly set to
type: .dynamicin itsPackage.swiftfile.
To update your unit tests:
In Xcode, open your
example/ios/Runner.xcworkspace.-
If you were using a CocoaPod dependency for tests, such as
OCMock, you'll want to remove it from yourPodfilefile.ios/Podfilerubytarget 'RunnerTests' do inherit! :search_paths pod 'OCMock', '3.5' endThen in the terminal, run
pod installin theplugin_name_ios/example/iosdirectory. -
Navigate to Package Dependencies for the project.

The project's package dependencies
-
Click the + button and add any test-only dependencies by searching for them in the top right search bar.

Search for test-only dependencies
-
Ensure the dependency is added to the
RunnerTestsTarget.
Ensure the dependency is added to the
RunnerTeststarget Click the Add Package button.
-
If you've explicitly set your plugin's library type to
.dynamicin itsPackage.swiftfile (not recommended by Apple), you'll also need to add it as a dependency to theRunnerTeststarget.Ensure
RunnerTestsBuild Phases has a Link Binary With Libraries build phase:
The
Link Binary With LibrariesBuild Phase in theRunnerTeststargetIf the build phase doesn't exist already, create one. Click the add button and then click New Link Binary With Libraries Phase.

Add
Link Binary With LibrariesBuild PhaseNavigate to Package Dependencies for the project.
Click the add button.
In the dialog that opens, click the Add Local... button.
Navigate to
plugin_name/plugin_name_ios/ios/plugin_name_iosand click the Add Package button.Ensure that it's added to the
RunnerTeststarget and click the Add Package button.
Ensure tests pass Product > Test.
Unless stated otherwise, the documentation on this site reflects Flutter 3.44.0. Page last updated on 2026-06-08. View source or report an issue.