Swift Package Manager for plugin authors
Flutter's Swift Package Manager integration has several benefits:
- Access to the Swift package ecosystem. Flutter plugins can use the growing ecosystem of Swift packages!
- Simplifies Flutter installation. Swift Package Manager is bundled with Xcode. In the future, you won’t need to install Ruby and CocoaPods to target iOS or macOS.
If you find a bug in Flutter's Swift Package Manager support, open an issue.
How to turn on Swift Package Manager
#Flutter's Swift Package Manager support is turned off by default. To turn it on:
-
Switch to Flutter's
main
channel:shflutter channel main --no-cache-artifacts
-
Upgrade to the latest Flutter SDK and download artifacts:
shflutter upgrade
-
Turn on the Swift Package Manager feature:
shflutter config --enable-swift-package-manager
Using the Flutter CLI to run an app migrates the project to add Swift Package Manager integration. This makes your project download the Swift packages that your Flutter plugins depend on. An app with Swift Package Manager integration requires Flutter version 3.24 or higher. To use an older Flutter version, you will need to remove Swift Package Manager integration from the app.
Flutter falls back to CocoaPods for dependencies that do not support Swift Package Manager yet.
How to turn off Swift Package Manager
#Disabling Swift Package Manager causes Flutter to use CocoaPods for all dependencies. However, Swift Package Manager remains integrated with your project. To remove Swift Package Manager integration completely from your project, follow the How to remove Swift Package Manager integration instructions.
Turn off for a single project
#In the project's pubspec.yaml
file, under the flutter
section,
add disable-swift-package-manager: true
.
# The following section is specific to Flutter packages.
flutter:
disable-swift-package-manager: true
This turns off Swift Package Manager for all contributors to this project.
Turn off globally for all projects
#Run the following command:
flutter config --no-enable-swift-package-manager
This turns off Swift Package Manager for the current user.
If a project is incompatible with Swift Package Manager, all contributors need to run this command.
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. Plugins that don't support CocoaPods won't be 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.
Replace plugin_name
throughout this guide with the name of your plugin.
The example below uses ios
, replace ios
with macos
/darwin
as applicable.
-
Start by creating a directory under the
ios
,macos
, and/ordarwin
directories. 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.swift
file: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("12.0"), .macOS("10.14") ], 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, see: // 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.swift
file.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("12.0"), .macOS("10.14") ],
-
Update the package, library, and target names in your
Package.swift
file.Package.swiftswiftlet package = Package( // TODO: Update your plugin name. name: "plugin_name", platforms: [ .iOS("12.0"), .macOS("10.14") ], 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, see: // 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.xcprivacy
file, move it toios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy
and uncomment the resource in thePackage.swift
file.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, see: // 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/Assets
toios/plugin_name/Sources/plugin_name
(or a subdirectory). Add the resource files to yourPackage.swift
file, if applicable. For more instructions, see https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package. -
Move all files from
ios/Classes
toios/plugin_name/Sources/plugin_name
. -
The
ios/Assets
,ios/Resources
, andios/Classes
directories 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.swift
file with any customizations you might need.-
Open the
ios/plugin_name/
directory in Xcode. -
In Xcode, open your
Package.swift
file. Verify Xcode doesn't produce any warnings or errors for this file. -
If your
ios/plugin_name.podspec
file has CocoaPodsdependency
s, add the corresponding Swift Package Manager dependencies to yourPackage.swift
file. -
If your package must be linked explicitly
static
ordynamic
(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.swift
file, see https://developer.apple.com/documentation/packagedescription.
-
-
Update your
ios/plugin_name.podspec
to 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
-
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-manager
-
Navigate to the plugin's example app.
shcd path/to/plugin/example/
-
Ensure the plugin's example app builds and runs.
shflutter run
-
Navigate 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-libraries
shpod 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-manager
-
Navigate to the plugin's example app.
shcd path/to/plugin/example/
-
Ensure the plugin's example app builds and runs.
shflutter run
-
Open the plugin's example app in Xcode. 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
/darwin
as applicable.
-
Start by creating a directory under the
ios
,macos
, and/ordarwin
directories. 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
.gitkeep
file 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.swift
file: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("12.0"), .macOS("10.14") ], 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, see: // 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.swift
file.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("12.0"), .macOS("10.14") ],
-
Update the package, library, and target names in your
Package.swift
file.Package.swiftswiftlet package = Package( // TODO: Update your plugin name. name: "plugin_name", platforms: [ .iOS("12.0"), .macOS("10.14") ], 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, see: // 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.xcprivacy
file, move it toios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy
and uncomment the resource in thePackage.swift
file.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, see: // 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/Assets
toios/plugin_name/Sources/plugin_name
(or a subdirectory). Add the resource files to yourPackage.swift
file, if applicable. For more instructions, see https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package. -
Move any public headers from
ios/Classes
toios/plugin_name/Sources/plugin_name/include/plugin_name
.-
If you're unsure which headers are public, check your
podspec
file'spublic_header_files
attribute. 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
pluginClass
defined in yourpubspec.yaml
file 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
modulemap
for 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
modulemap
for Swift Package Manager but keep it for CocoaPods, exclude themodulemap
and umbrella header in the plugin'sPackage.swift
file.The example below assumes the
modulemap
and umbrella header are located in theios/plugin_name/Sources/plugin_name/include
directory.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; #endif
If you would like to use a custom
modulemap
with your Swift package, refer to Swift Package Manager's documentation. -
Move all remaining files from
ios/Classes
toios/plugin_name/Sources/plugin_name
. -
The
ios/Assets
,ios/Resources
, andios/Classes
directories 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.m
-
After:
ios/plugin_name/Sources/plugin_name/ └── include/plugin_name/ └── PublicHeaderFile.h └── ImplementationFile.m
In this example, the import statements in
ImplementationFile.m
should 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
objcHeaderOut
file is no longer within the same directory as theobjcSourceOut
, you can change the#import
usingObjcOptions.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.swift
file with any customizations you might need.-
Open the
ios/plugin_name/
directory in Xcode. -
In Xcode, open your
Package.swift
file. Verify Xcode doesn't produce any warnings or errors for this file. -
If your
ios/plugin_name.podspec
file has CocoaPodsdependency
s, add the corresponding Swift Package Manager dependencies to yourPackage.swift
file. -
If your package must be linked explicitly
static
ordynamic
(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.swift
file, see https://developer.apple.com/documentation/packagedescription.
-
-
Update your
ios/plugin_name.podspec
to 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/include
directory only contains a.gitkeep
, you'll want update your.gitignore
to include the following:.gitignoretext!.gitkeep
Run
flutter pub publish --dry-run
to ensure theinclude
directory 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-manager
-
Navigate to the plugin's example app.
shcd path/to/plugin/example/
-
Ensure the plugin's example app builds and runs.
shflutter run
-
Navigate 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-libraries
shpod 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-manager
-
Navigate to the plugin's example app.
shcd path/to/plugin/example/
-
Ensure the plugin's example app builds and runs.
shflutter run
-
Open the plugin's example app in Xcode. 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.
-
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: .dynamic
in itsPackage.swift
file.
To update your unit tests:
-
Open your
example/ios/Runner.xcworkspace
in Xcode. -
If you were using a CocoaPod dependency for tests, such as
OCMock
, you'll want to remove it from yourPodfile
file.ios/Podfilerubytarget 'RunnerTests' do inherit! :search_paths pod 'OCMock', '3.5' end
Then in the terminal, run
pod install
in theplugin_name_ios/example/ios
directory. -
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
RunnerTests
Target.
Ensure the dependency is added to the RunnerTests
target -
Click the Add Package button.
-
If you've explicitly set your plugin's library type to
.dynamic
in itsPackage.swift
file (not recommended by Apple), you'll also need to add it as a dependency to theRunnerTests
target.-
Ensure
RunnerTests
Build Phases has a Link Binary With Libraries build phase:
The Link Binary With Libraries
Build Phase in theRunnerTests
targetIf the build phase doesn't exist already, create one. Click the add and then click New Link Binary With Libraries Phase.
Add Link Binary With Libraries
Build Phase -
Navigate to Package Dependencies for the project.
-
Click add.
-
In the dialog that opens, click the Add Local... button.
-
Navigate to
plugin_name/plugin_name_ios/ios/plugin_name_ios
and click the Add Package button. -
Ensure that it's added to the
RunnerTests
target and click the Add Package button.
-
-
Ensure tests pass Product > Test.
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-08-16。 查看文档源码 或者 为本页面内容提出建议。