Swift Package Manager 給插件作者
Flutter 整合 Swift Package Manager 有以下幾項優點:
- 可存取 Swift 套件生態系統。 Flutter 插件現在可以使用日益壯大的 Swift packages 生態系統!
- 簡化 Flutter 安裝流程。 Swift Package Manager 已隨 Xcode 一同安裝。 未來,你不需要再安裝 Ruby 和 CocoaPods 來支援 iOS 或 macOS。
How to turn on Swift Package Manager
#Flutter's Swift Package Manager support is turned off by default. To turn it on:
Upgrade to the latest Flutter SDK:
shflutter upgradeTurn 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: trueThis 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-managerThis 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.
如何為現有 Flutter 插件新增 Swift Package Manager 支援
#本指南說明如何為已支援 CocoaPods 的插件新增 Swift Package Manager 支援。 這可確保該插件可被所有 Flutter 專案使用。
在另行通知前,Flutter 插件應同時支援 Swift Package Manager 和 CocoaPods。
Swift Package Manager 的導入將會是漸進式的。 尚未支援 CocoaPods 的插件,將無法被尚未遷移至 Swift Package Manager 的專案使用。 而不支援 Swift Package Manager 的插件,則可能會對已遷移的專案造成問題。
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/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: [], 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.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, 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.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, 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/Assetstoios/plugin_name/Sources/plugin_name(or a subdirectory). Add the resource files to yourPackage.swiftfile, if applicable. For more instructions, see https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package.Move all 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 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.Open the
ios/plugin_name/directory in Xcode.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, see https://developer.apple.com/documentation/packagedescription.
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") #endifIf your
.gitignoredoesn't include.build/and.swiftpm/directories, you'll want to update your.gitignoreto include:.gitignoretext.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 runOpen 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/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/ └── .gitkeepUse 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 // (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.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, 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.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, 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/Assetstoios/plugin_name/Sources/plugin_name(or a subdirectory). Add the resource files to yourPackage.swiftfile, if applicable. For more instructions, see 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.Open the
ios/plugin_name/directory in Xcode.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, see 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 update your.gitignoreto include the following:.gitignoretext!.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 runOpen 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.
</plugin_name>
如何更新插件範例 App 的單元測試
#如果你的插件有原生的 XCTest 單元測試,且符合以下其中一項情況,則你可能需要更新測試以支援 Swift Package Manager:
- 你的測試使用了 CocoaPod 相依套件。
- 你的插件在
Package.swift檔案中明確設定為type: .dynamic。
要更新你的單元測試:
在 Xcode 中開啟你的
example/ios/Runner.xcworkspace。如果你原本在測試中使用了 CocoaPod 相依套件(例如
OCMock), 請將其從Podfile檔案中移除。ios/Podfilerubytarget 'RunnerTests' do inherit! :search_paths pod 'OCMock', '3.5' end然後在終端機中,在
plugin_name_ios/example/ios目錄下執行pod install。導覽至專案的 Package Dependencies(套件相依性)。

專案的套件相依性 點擊 + 按鈕,並在右上角的搜尋欄中搜尋,新增任何僅用於測試的相依套件。

搜尋僅用於測試的相依套件 確認該相依套件已加入至
RunnerTests目標(Target)。
確保相依套件已加入至 RunnerTests目標點擊 Add Package(新增套件)按鈕。
如果你已在
Package.swift檔案中明確將 plugin 的 library type 設為.dynamic(Apple 不建議這麼做), 你還需要將其加入RunnerTests目標的相依套件中。確認
RunnerTests的 Build Phases(建置階段)中有 Link Binary With Libraries 的建置階段:
Link Binary With Libraries建置階段於RunnerTests目標如果該建置階段尚未存在,請新增一個。 點擊 add,然後點擊 New Link Binary With Libraries Phase(新增連結二進位檔與函式庫階段)。

新增 Link Binary With Libraries建置階段導覽至專案的 Package Dependencies(套件相依性)。
點擊 add。
在開啟的對話視窗中,點擊 Add Local...(新增本地...)按鈕。
導覽至
plugin_name/plugin_name_ios/ios/plugin_name_ios,然後點擊 Add Package(新增套件)按鈕。確認已將其加入
RunnerTests目標,並點擊 Add Package(新增套件)按鈕。
確認測試通過:Product > Test。