适用于插件开发者的 Swift Package Manager
Flutter 的 Swift Package Manager 集成有以下几个优点
- 提供访问 Swift 包生态系统的能力。Flutter 插件可以使用不断发展的Swift 包生态系统!
- 简化 Flutter 安装。Swift Package Manager 与 Xcode 捆绑在一起。将来,你无需安装 Ruby 和 CocoaPods 即可面向 iOS 或 macOS。
如何启用 Swift Package Manager
#Flutter 对 Swift Package Manager 的支持默认处于禁用状态。要启用它,请执行以下操作:
升级到最新的 Flutter SDK
shflutter upgrade
启用 Swift Package Manager 功能
shflutter config --enable-swift-package-manager
使用 Flutter CLI 运行应用会迁移项目以添加 Swift Package Manager 集成。这会使你的项目下载 Flutter 插件所依赖的 Swift 包。集成 Swift Package Manager 的应用需要 Flutter 3.24 或更高版本。要使用旧版 Flutter,你需要从应用中移除 Swift Package Manager 集成。
对于尚不支持 Swift Package Manager 的依赖项,Flutter 会回退到 CocoaPods。
如何禁用 Swift Package Manager
#禁用 Swift Package Manager 会导致 Flutter 对所有依赖项使用 CocoaPods。但是,Swift Package Manager 仍会与你的项目集成。要从项目中完全移除 Swift Package Manager 集成,请遵循如何移除 Swift Package Manager 集成的说明。
禁用单个项目
#在项目的 pubspec.yaml
文件中,在 flutter
部分下,添加 disable-swift-package-manager: true
。
# The following section is specific to Flutter packages.
flutter:
disable-swift-package-manager: true
这会为该项目的所有贡献者禁用 Swift Package Manager。
全局禁用所有项目
#运行以下命令
flutter config --no-enable-swift-package-manager
这会为当前用户禁用 Swift Package Manager。
如果一个项目与 Swift Package Manager 不兼容,所有贡献者都需要运行此命令。
如何为现有 Flutter 插件添加 Swift Package Manager 支持
#本指南展示了如何为已支持 CocoaPods 的插件添加 Swift Package Manager 支持。这可确保所有 Flutter 项目都能使用该插件。
在另行通知之前,Flutter 插件应同时支持 Swift Package Manager 和 CocoaPods。
Swift Package Manager 的采用将是渐进的。不支持 CocoaPods 的插件将无法被尚未迁移到 Swift Package Manager 的项目使用。不支持 Swift Package Manager 的插件可能会给已迁移的项目带来问题。
在本指南中,将所有 plugin_name
替换为你的插件名称。以下示例使用 ios
,请根据需要将其替换为 macos
/darwin
。
首先在
ios
、macos
和/或darwin
目录下创建一个新目录。将此新目录命名为平台包的名称。plugin_name/ios/ ├── ... └── plugin_name/
在此新目录中,创建以下文件/目录:
Package.swift
(文件)Sources
(目录)Sources/plugin_name
(目录)
你的插件应该看起来像:
plugin_name/ios/ ├── ... └── plugin_name/ ├── Package.swift └── Sources/plugin_name/
在
Package.swift
文件中使用以下模板: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 ] ) ] )
更新
Package.swift
文件中的支持平台。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") ],
更新
Package.swift
文件中的包、库和目标名称。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 ] ) ] )
如果你的插件有
PrivacyInfo.xcprivacy
文件,请将其移动到ios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy
并在Package.swift
文件中取消注释该资源。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 ],
将所有资源文件从
ios/Assets
移动到ios/plugin_name/Sources/plugin_name
(或其子目录)。如果适用,将资源文件添加到你的Package.swift
文件中。有关更多说明,请参阅https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package。将所有文件从
ios/Classes
移动到ios/plugin_name/Sources/plugin_name
。ios/Assets
、ios/Resources
和ios/Classes
目录现在应为空,可以删除。如果你的插件使用 Pigeon,请更新你的 Pigeon 输入文件。
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(),
根据需要更新你的
Package.swift
文件。在 Xcode 中打开
ios/plugin_name/
目录。在 Xcode 中,打开你的
Package.swift
文件。验证 Xcode 是否没有针对此文件生成任何警告或错误。如果你的
ios/plugin_name.podspec
文件有 CocoaPodsdependency
,请将相应的 Swift Package Manager 依赖项添加到你的Package.swift
文件中。如果你的包必须显式链接为
static
或dynamic
(Apple 不推荐),则更新 Product 以定义类型。Package.swiftswiftproducts: [ .library(name: "plugin-name", type: .static, targets: ["plugin_name"]) ],
进行其他任何自定义。有关如何编写
Package.swift
文件的更多信息,请参阅https://developer.apple.com/documentation/packagedescription。
更新你的
ios/plugin_name.podspec
以指向新路径。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']}
更新从 bundle 加载资源的方式,以使用
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
如果你的
.gitignore
不包含.build/
和.swiftpm/
目录,你需要更新你的.gitignore
以包含:.gitignoretext.build/ .swiftpm/
将你的插件更改提交到版本控制系统。
验证插件是否仍与 CocoaPods 兼容。
禁用 Swift Package Manager。
shflutter config --no-enable-swift-package-manager
导航到插件的示例应用。
shcd path/to/plugin/example/
确保插件的示例应用能够构建和运行。
shflutter run
导航到插件的顶层目录。
shcd path/to/plugin/
运行 CocoaPods 验证检查。
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
验证插件是否与 Swift Package Manager 兼容。
启用 Swift Package Manager。
shflutter config --enable-swift-package-manager
导航到插件的示例应用。
shcd path/to/plugin/example/
确保插件的示例应用能够构建和运行。
shflutter run
在 Xcode 中打开插件的示例应用。确保 Package Dependencies 显示在左侧的 Project Navigator 中。
验证测试是否通过。
如果你的插件有原生单元测试 (XCTest),请确保你还更新了插件示例应用中的单元测试。
遵循测试插件的说明。
在本指南中,将所有 plugin_name
替换为你的插件名称。以下示例使用 ios
,请根据需要将其替换为 macos
/darwin
。
首先在
ios
、macos
和/或darwin
目录下创建一个新目录。将此新目录命名为平台包的名称。plugin_name/ios/ ├── ... └── plugin_name/
在此新目录中,创建以下文件/目录:
Package.swift
(文件)Sources
(目录)Sources/plugin_name
(目录)Sources/plugin_name/include
(目录)Sources/plugin_name/include/plugin_name
(目录)Sources/plugin_name/include/plugin_name/.gitkeep
(文件)- 此文件确保目录被提交。如果目录中添加了其他文件,则可以删除
.gitkeep
文件。
- 此文件确保目录被提交。如果目录中添加了其他文件,则可以删除
你的插件应该看起来像:
plugin_name/ios/ ├── ... └── plugin_name/ ├── Package.swift └── Sources/plugin_name/include/plugin_name/ └── .gitkeep
在
Package.swift
文件中使用以下模板: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") ] ) ] )
更新
Package.swift
文件中的支持平台。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") ],
更新
Package.swift
文件中的包、库和目标名称。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") ] ) ] )
如果你的插件有
PrivacyInfo.xcprivacy
文件,请将其移动到ios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy
并在Package.swift
文件中取消注释该资源。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 ],
将所有资源文件从
ios/Assets
移动到ios/plugin_name/Sources/plugin_name
(或其子目录)。如果适用,将资源文件添加到你的Package.swift
文件中。有关更多说明,请参阅https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package。将所有公共头文件从
ios/Classes
移动到ios/plugin_name/Sources/plugin_name/include/plugin_name
。如果你不确定哪些头文件是公共的,请检查你的
podspec
文件的public_header_files
属性。如果未指定此属性,则你的所有头文件都是公共的。你应该考虑是否希望所有头文件都为公共的。在你的
pubspec.yaml
文件中定义的pluginClass
必须是公共的且在此目录中。
处理
modulemap
。如果你的插件没有
modulemap
,请跳过此步骤。如果你正在使用
modulemap
为 CocoaPods 创建一个 Test 子模块,请考虑为 Swift Package Manager 移除它。请注意,这将使所有公共头文件通过该模块可用。要为 Swift Package Manager 移除
modulemap
但为 CocoaPods 保留它,请在插件的Package.swift
文件中排除modulemap
和 umbrella header。以下示例假设
modulemap
和 umbrella header 位于ios/plugin_name/Sources/plugin_name/include
目录中。Package.swiftswift.target( name: "plugin_name", dependencies: [], exclude: ["include/cocoapods_plugin_name.modulemap", "include/plugin_name-umbrella.h"],
如果你想让你的单元测试同时兼容 CocoaPods 和 Swift Package Manager,你可以尝试以下方法:
Tests/TestFile.mobjc@import plugin_name; @import plugin_name.Test; #if __has_include(<plugin_name plugin_name-umbrella.h="">) @import plugin_name.Test; #endif
如果你想在 Swift 包中使用自定义的
modulemap
,请参阅Swift Package Manager 的文档。将所有剩余文件从
ios/Classes
移动到ios/plugin_name/Sources/plugin_name
。ios/Assets
、ios/Resources
和ios/Classes
目录现在应为空,可以删除。如果你的头文件不再与实现文件位于同一目录中,则应更新你的 import 语句。
例如,设想以下迁移:
之前
ios/Classes/ ├── PublicHeaderFile.h └── ImplementationFile.m
之后
ios/plugin_name/Sources/plugin_name/ └── include/plugin_name/ └── PublicHeaderFile.h └── ImplementationFile.m
在此示例中,
ImplementationFile.m
中的 import 语句应更新为:Sources/plugin_name/ImplementationFile.mobjc#import "PublicHeaderFile.h" #import "./include/plugin_name/PublicHeaderFile.h"
如果你的插件使用 Pigeon,请更新你的 Pigeon 输入文件。
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',
如果你的
objcHeaderOut
文件不再与objcSourceOut
位于同一目录中,你可以使用ObjcOptions.headerIncludePath
更改#import
。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',
运行 Pigeon 以使用最新配置重新生成其代码。
根据需要更新你的
Package.swift
文件。在 Xcode 中打开
ios/plugin_name/
目录。在 Xcode 中,打开你的
Package.swift
文件。验证 Xcode 是否没有针对此文件生成任何警告或错误。如果你的
ios/plugin_name.podspec
文件有 CocoaPodsdependency
,请将相应的 Swift Package Manager 依赖项添加到你的Package.swift
文件中。如果你的包必须显式链接为
static
或dynamic
(Apple 不推荐),则更新 Product 以定义类型。Package.swiftswiftproducts: [ .library(name: "plugin-name", type: .static, targets: ["plugin_name"]) ],
进行其他任何自定义。有关如何编写
Package.swift
文件的更多信息,请参阅https://developer.apple.com/documentation/packagedescription。
更新你的
ios/plugin_name.podspec
以指向新路径。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']}
更新从 bundle 加载资源的方式,以使用
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"];
如果你的
ios/plugin_name/Sources/plugin_name/include
目录只包含一个.gitkeep
文件,你需要更新你的.gitignore
以包含以下内容:.gitignoretext!.gitkeep
运行
flutter pub publish --dry-run
以确保include
目录被发布。将你的插件更改提交到版本控制系统。
验证插件是否仍与 CocoaPods 兼容。
禁用 Swift Package Manager
shflutter config --no-enable-swift-package-manager
导航到插件的示例应用。
shcd path/to/plugin/example/
确保插件的示例应用能够构建和运行。
shflutter run
导航到插件的顶层目录。
shcd path/to/plugin/
运行 CocoaPods 验证检查
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
验证插件是否与 Swift Package Manager 兼容。
启用 Swift Package Manager
shflutter config --enable-swift-package-manager
导航到插件的示例应用。
shcd path/to/plugin/example/
确保插件的示例应用能够构建和运行。
shflutter run
在 Xcode 中打开插件的示例应用。确保 Package Dependencies 显示在左侧的 Project Navigator 中。
验证测试是否通过。
如果你的插件有原生单元测试 (XCTest),请确保你还更新了插件示例应用中的单元测试。
遵循测试插件的说明。
</plugin_name>
如何更新插件示例应用中的单元测试
#如果你的插件有原生 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
目标。
确保依赖项已添加到 RunnerTests
目标点击 Add Package 按钮。
如果你已在插件的
Package.swift
文件中显式将库类型设置为.dynamic
(Apple 不推荐),你还需要将其作为依赖项添加到RunnerTests
目标中。确保
RunnerTests
的 Build Phases 中包含 Link Binary With Libraries 构建阶段。
RunnerTests
目标中的Link Binary With Libraries
构建阶段如果构建阶段尚不存在,请创建一个。点击 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。