跳转至正文

面向插件作者的 Swift Package Manager

如何为 iOS 和 macOS 插件添加 Swift Package Manager 兼容性

Flutter 的 Swift Package Manager 集成带来多项好处:

  1. Provides access to the Swift package ecosystem. Flutter plugins can use the growing ecosystem of Swift packages!

  2. 可访问 Swift 包生态。Flutter 插件可使用不断增长的 Swift packages(Swift 包)生态!

  3. 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.

  4. 简化 Flutter 安装。Swift Package Manager 随 Xcode 捆绑提供。未来 targeting iOS 或 macOS 时可能无需再安装 Ruby 和 CocoaPods。

如何开启 Swift Package Manager

#

默认情况下,Flutter 的 Swift Package Manager 支持处于关闭状态。要开启它:

  1. 升级到最新的 Flutter SDK:

    sh
    flutter upgrade
    
  2. 开启 Swift Package Manager 功能:

    sh
    flutter 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 小节下的 config 子小节里,将 enable-swift-package-manager 设为 false

pubspec.yaml
yaml
# The following section is specific to Flutter packages.
flutter:
  config:
    enable-swift-package-manager: false

这会为参与该项目的所有贡献者关闭 Swift Package Manager。

为所有项目全局关闭

#

运行以下命令:

sh
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,请酌情将 ios 替换为 macos/darwin

  1. 开启 Swift Package Manager 功能

  2. 首先在 iosmacos 和/或 darwin 目录下创建一个目录。将该新目录命名为平台包的名称。

    • plugin_name/
      • ios/
        • plugin_name/
  3. 在此新目录中,创建以下文件/目录:

    • Package.swift (file)

    • Sources (directory)

    • Sources/plugin_name (directory)

    • Package.swift(文件)

    • Sources(目录)

    • Sources/plugin_name(目录)

    你的插件结构应如下所示:

    • plugin_name/
      • ios/
        • plugin_name/
          • Package.swift
          • Sources/
            • plugin_name/
  4. Package.swift 文件中使用以下模板:

    Package.swift
    swift
    // 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, 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
                ]
            )
        ]
    )
    
  5. Package.swift 文件中更新支持的平台

    Package.swift
    swift
        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")
        ],
    
  6. Package.swift 文件中更新包、库和目标名称。

    Package.swift
    swift
    let 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
                ]
            )
        ]
    )
    
  7. 如果你的插件有 PrivacyInfo.xcprivacy 文件,请将其移动到 ios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy,并在 Package.swift 文件中取消注释该资源。

    Package.swift
    swift
                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
                ],
    
  8. ios/Assets 中的资源文件移动到 ios/plugin_name/Sources/plugin_name(或其子目录)。如适用,将资源文件添加到 Package.swift 文件中。更多说明请参阅 https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package

  9. ios/Classes 中的所有文件移动到 ios/plugin_name/Sources/plugin_name

  10. Flutter 3.41 新增! 将 FlutterFramework 添加为依赖并更新 Dart/Flutter 版本。

    更新 Package.swift 以包含 FlutterFramework

    Package.swift
    swift
    dependencies: [
        .package(name: "FlutterFramework", path: "../FlutterFramework")
    ],
    targets: [
        .target(
            // TODO: Update your target name.
            name: "plugin_name",
            dependencies: [
                .product(name: "FlutterFramework", package: "FlutterFramework")
            ],
    

    pubspec.yaml 中,将版本更新为:

    pubspec.yaml
    yaml
    environment:
      sdk: ^3.11.0
      flutter: ">=3.41.0"
    
  11. ios/Assetsios/Resourcesios/Classes 目录现在应为空,可以删除。

  12. 如果你的插件使用 Pigeon,请更新 Pigeon 输入文件。

    pigeons/messages.dart
    dart
    kotlinOptions: 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(),
    
  13. 根据需要进行自定义,更新 Package.swift 文件。

    1. 在 Xcode 中打开 ios/plugin_name/ 目录。

    2. 在 Xcode 中打开 Package.swift 文件。确认 Xcode 不会对此文件产生警告或错误。

    3. 如果 ios/plugin_name.podspec 文件包含 CocoaPods dependency,请将对应的 Swift Package Manager 依赖 添加到 Package.swift 文件。

    4. 如果包必须显式以 staticdynamic 链接(Apple 不推荐),请更新 Product 以定义类型:

      Package.swift
      swift
      products: [
          .library(name: "plugin-name", type: .static, targets: ["plugin_name"])
      ],
      
    5. 进行其他自定义。有关如何编写 Package.swift 文件的更多信息,请参阅 https://developer.apple.com/documentation/packagedescription

  14. 更新 ios/plugin_name.podspec,使其指向新路径。

    ios/plugin_name.podspec
    ruby
    s.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']}
    
  15. 更新从 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
    
  16. 如果 .gitignore 未包含 .build/.swiftpm/ 目录,你需要更新 .gitignore 以包含:

    .gitignore
    .build/
    .swiftpm/
    

    将插件的更改提交到版本控制系统。

  17. 验证插件在 CocoaPods 下仍能正常工作。

    1. 关闭 Swift Package Manager:

      sh
      flutter config --no-enable-swift-package-manager
      
    2. 进入插件的示例应用目录。

      sh
      cd path/to/plugin/example/
      
    3. 确保插件的示例应用能构建并运行。

      sh
      flutter run
      
    4. 进入插件的顶层目录。

      sh
      cd path/to/plugin/
      
    5. 运行 CocoaPods 验证 lint:

      sh
      pod lib lint ios/plugin_name.podspec  --configuration=Debug --skip-tests --use-modular-headers --use-libraries
      
      sh
      pod lib lint ios/plugin_name.podspec  --configuration=Debug --skip-tests --use-modular-headers
      
  18. 验证插件在 Swift Package Manager 下能正常工作。

    1. 开启 Swift Package Manager:

      sh
      flutter config --enable-swift-package-manager
      
    2. 进入插件的示例应用目录。

      sh
      cd path/to/plugin/example/
      
    3. 确保插件的示例应用能构建并运行。

      sh
      flutter run
      
    4. 在 Xcode 中打开插件的示例应用。确保左侧 Project Navigator(项目导航器)中显示 Package Dependencies(包依赖)。

  19. 验证测试通过。

在本指南全文将 plugin_name 替换为你的插件名称。以下示例使用 ios,请酌情将 ios 替换为 macos/darwin

  1. 开启 Swift Package Manager 功能

  2. 首先在 iosmacos 和/或 darwin 目录下创建一个目录。将该新目录命名为平台包的名称。

    • plugin_name/
      • ios/
        • plugin_name/
  3. 在此新目录中,创建以下文件/目录:

    • 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.
    • 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/
  4. Package.swift 文件中使用以下模板:

    Package.swift
    swift
    // 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")
                ]
            )
        ]
    )
    
  5. Package.swift 文件中更新支持的平台

    Package.swift
    swift
        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")
        ],
    
  6. Package.swift 文件中更新包、库和目标名称。

    Package.swift
    swift
    let 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")
                ]
            )
        ]
    )
    
  7. 如果你的插件有 PrivacyInfo.xcprivacy 文件,请将其移动到 ios/plugin_name/Sources/plugin_name/PrivacyInfo.xcprivacy,并在 Package.swift 文件中取消注释该资源。

    Package.swift
    swift
                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
                ],
    
  8. ios/Assets 中的资源文件移动到 ios/plugin_name/Sources/plugin_name(或其子目录)。如适用,将资源文件添加到 Package.swift 文件中。更多说明请参阅 https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package

  9. ios/Classes 中的公共头文件移动到 ios/plugin_name/Sources/plugin_name/include/plugin_name

    • If you're unsure which headers are public, check your podspec file's public_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.

    • 如果不确定哪些头文件是公共的,请检查 podspec 文件中的 public_header_files 属性。如果未指定该属性,则所有头文件都是公共的。你应斟酌是否希望所有头文件都是公共的。

    • The pluginClass defined in your pubspec.yaml file must be public and within this directory.

    • pubspec.yaml 文件中定义的 pluginClass 必须是公共的,且位于此目录中。

  10. 处理 modulemap

    如果你的插件没有 modulemap,请跳过此步骤。

    如果你为 CocoaPods 使用 modulemap 来创建 Test 子模块,请考虑为 Swift Package Manager 将其移除。请注意,这会使所有公共头文件通过该模块可用。

    若要为 Swift Package Manager 移除 modulemap 但为 CocoaPods 保留,请在插件的 Package.swift 文件中排除 modulemap 和 umbrella 头文件。

    以下示例假定 modulemap 和 umbrella 头文件位于 ios/plugin_name/Sources/plugin_name/include 目录。

    Package.swift
    swift
    .target(
        name: "plugin_name",
        dependencies: [],
        exclude: ["include/cocoapods_plugin_name.modulemap", "include/plugin_name-umbrella.h"],
    

    如果你希望单元测试同时兼容 CocoaPods 和 Swift Package Manager,可以尝试以下做法:

    Tests/TestFile.m
    objc
    @import plugin_name;
    @import plugin_name.Test;
    #if __has_include(<plugin_name/plugin_name-umbrella.h>)
      @import plugin_name.Test;
    #endif
    

    如果你希望将自定义 modulemap 用于 Swift 包,请参阅 Swift Package Manager 文档

  11. ios/Classes 中所有剩余文件移动到 ios/plugin_name/Sources/plugin_name

  12. ios/Assetsios/Resourcesios/Classes 目录现在应为空,可以删除。

  13. 如果头文件不再与实现文件位于同一目录,你应更新 import 语句。

    例如,假设进行以下迁移:

    • Before:

    • 迁移前:

      ios/Classes/
      ├── PublicHeaderFile.h
      └── ImplementationFile.m
      
    • After:

    • 迁移后:

      ios/plugin_name/Sources/plugin_name/
      └── include/plugin_name/
         └── PublicHeaderFile.h
      └── ImplementationFile.m
      

    在此示例中,应更新 ImplementationFile.m 中的 import 语句:

    Sources/plugin_name/ImplementationFile.m
    objc
    #import "PublicHeaderFile.h"
    #import "./include/plugin_name/PublicHeaderFile.h"
    
  14. 如果你的插件使用 Pigeon,请更新 Pigeon 输入文件。

    pigeons/messages.dart
    dart
    javaOptions: 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.dart
    dart
    javaOptions: 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,以最新配置重新生成代码。

  15. 根据需要进行自定义,更新 Package.swift 文件。

    1. 在 Xcode 中打开 ios/plugin_name/ 目录。

    2. 在 Xcode 中打开 Package.swift 文件。确认 Xcode 不会对此文件产生警告或错误。

    3. 如果 ios/plugin_name.podspec 文件包含 CocoaPods dependency,请将对应的 Swift Package Manager 依赖 添加到 Package.swift 文件。

    4. 如果包必须显式以 staticdynamic 链接(Apple 不推荐),请更新 Product 以定义类型:

      Package.swift
      swift
      products: [
          .library(name: "plugin-name", type: .static, targets: ["plugin_name"])
      ],
      
    5. 进行其他自定义。有关如何编写 Package.swift 文件的更多信息,请参阅 https://developer.apple.com/documentation/packagedescription

  16. 更新 ios/plugin_name.podspec,使其指向新路径。

    ios/plugin_name.podspec
    ruby
    s.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']}
    
  17. 更新从 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"];
    
  18. 如果 ios/plugin_name/Sources/plugin_name/include 目录仅包含 .gitkeep,你需要更新 .gitignore 以包含以下内容:

    .gitignore
    !.gitkeep
    

    运行 flutter pub publish --dry-run,确保 include 目录会被发布。

  19. 将插件的更改提交到版本控制系统。

  20. 验证插件在 CocoaPods 下仍能正常工作。

    1. 关闭 Swift Package Manager:

      sh
      flutter config --no-enable-swift-package-manager
      
    2. 进入插件的示例应用目录。

      sh
      cd path/to/plugin/example/
      
    3. 确保插件的示例应用能构建并运行。

      sh
      flutter run
      
    4. 进入插件的顶层目录。

      sh
      cd path/to/plugin/
      
    5. 运行 CocoaPods 验证 lint:

      sh
      pod lib lint ios/plugin_name.podspec  --configuration=Debug --skip-tests --use-modular-headers --use-libraries
      
      sh
      pod lib lint ios/plugin_name.podspec  --configuration=Debug --skip-tests --use-modular-headers
      
  21. 验证插件在 Swift Package Manager 下能正常工作。

    1. 开启 Swift Package Manager:

      sh
      flutter config --enable-swift-package-manager
      
    2. 进入插件的示例应用目录。

      sh
      cd path/to/plugin/example/
      
    3. 确保插件的示例应用能构建并运行。

      sh
      flutter run
      
    4. 在 Xcode 中打开插件的示例应用。确保左侧 Project Navigator(项目导航器)中显示 Package Dependencies(包依赖)。

  22. 验证测试通过。

#

若插件包含示例,建议在示例应用中将插件添加为本地包。非必须,但在示例应用中编辑插件源码时能提供更好的 Xcode 支持。请参阅 issue #179032

将插件添加为本地包

#
  1. In a terminal navigate to my_plugin.

  2. 在终端中进入 my_plugin

  3. Run the following command to open the example app's workspace in Xcode, (replace ios with macos if your plugin targets macOS):

  4. 运行以下命令在 Xcode 中打开示例应用的工作区(若插件面向 macOS,将 ios 替换为 macos):

bash
open example/ios/Runner.xcworkspace
  1. Right click Flutter > Add Files to "Runner".

  2. 右键 Flutter > Add Files to "Runner"(添加文件到 Runner)。

    Add Files to Runner

  3. Select my_plugin/ios/my_plugin (or macos or darwin depending on what platforms your plugin supports).

  4. 选择 my_plugin/ios/my_plugin(或根据插件支持的平台选择 macosdarwin)。

  5. Make sure "Reference files in place" is selected (it should be the default), and click Finish.

  6. 确保选中「Reference files in place」(引用文件位置,通常为默认),然后点击 Finish(完成)。

    Select Reference files in place

这样会将插件添加为本地包,但会以绝对路径引用,不利于分发。要改为相对路径,请按以下步骤操作。

改为相对路径

#
  1. Copy "Full Path" for plugin from the File Inspector.

  2. 从文件检查器复制插件的「Full Path」(完整路径)。

    Copy Full Path

  3. In terminal: open -a Xcode example/ios/Runner.xcodeproj/project.pbxproj

  4. 在终端执行: open -a Xcode example/ios/Runner.xcodeproj/project.pbxproj

  5. Find the following:

  6. 找到以下内容:

    path = [COPIED FULL PATH]; sourceTree = "<absolute>"
    

    例如:

    path = /Users/username/path/to/my_plugin/ios/my_plugin; sourceTree = "<absolute>"
    
  7. And replace with relative path:

  8. 并替换为相对路径:

    path = ../../ios/my_plugin; sourceTree = "<group>"
    

    (按需将 ios 调整为 macosdarwin。)

如何更新插件示例应用中的单元测试

#

若插件有原生 XCTests,在以下任一情况下可能需要更新以配合 Swift Package Manager:

  • You're using a CocoaPod dependency for the test.

  • 测试使用了 CocoaPod 依赖。

  • Your plugin is explicitly set to type: .dynamic in its Package.swift file.

  • 插件在 Package.swift 中显式设为 type: .dynamic

要更新单元测试:

  1. Open your example/ios/Runner.xcworkspace in Xcode.

  2. 在 Xcode 中打开 example/ios/Runner.xcworkspace

  3. If you were using a CocoaPod dependency for tests, such as OCMock, you'll want to remove it from your Podfile file.

  4. 若测试曾使用 CocoaPod 依赖(如 OCMock),请从 Podfile 中移除。

    ios/Podfile
    ruby
    target 'RunnerTests' do
      inherit! :search_paths
    
      pod 'OCMock', '3.5'
    end
    

    然后在终端于 plugin_name_ios/example/ios 目录运行 pod install

  5. Navigate to Package Dependencies for the project.

  6. 导航到项目的 Package Dependencies(包依赖)。

    The project's package dependencies

    The project's package dependencies

    项目的包依赖

    项目的包依赖

  7. Click the + button and add any test-only dependencies by searching for them in the top right search bar.

  8. 点击 + 按钮,在右上角搜索栏搜索并添加仅用于测试的依赖。

    Search for test-only dependencies

    Search for test-only dependencies

    搜索仅用于测试的依赖

    搜索仅用于测试的依赖

  9. Ensure the dependency is added to the RunnerTests Target.

  10. 确保依赖已添加到 RunnerTests Target。

    Ensure the dependency is added to the `RunnerTests` target

    Ensure the dependency is added to the RunnerTests target

    确保依赖已添加到 `RunnerTests` target

    确保依赖已添加到 RunnerTests target

  11. Click the Add Package button.

  12. 点击 Add Package(添加包)按钮。

  13. If you've explicitly set your plugin's library type to .dynamic in its Package.swift file (not recommended by Apple), you'll also need to add it as a dependency to the RunnerTests target.

  14. 若在 Package.swift 中将插件库类型显式设为 .dynamicApple 不推荐),还需将其添加为 RunnerTests target 的依赖。

    1. Ensure RunnerTests Build Phases has a Link Binary With Libraries build phase:

    2. 确保 RunnerTestsBuild Phases(构建阶段)包含 Link Binary With Libraries(链接二进制与库)构建阶段:

      The `Link Binary With Libraries` Build Phase in the `RunnerTests` target

      The Link Binary With Libraries Build Phase in the RunnerTests target

      `RunnerTests` target 中的 `Link Binary With Libraries` 构建阶段

      RunnerTests target 中的 Link Binary With Libraries 构建阶段

      若尚无该构建阶段,请创建:点击 add 按钮,再点击 New Link Binary With Libraries Phase

      Add `Link Binary With Libraries` Build Phase

      Add Link Binary With Libraries Build Phase

      添加 `Link Binary With Libraries` 构建阶段

      添加 Link Binary With Libraries 构建阶段

    3. Navigate to Package Dependencies for the project.

    4. 导航到项目的 Package Dependencies

    5. Click the add button.

    6. 点击 add 按钮。

    7. In the dialog that opens, click the Add Local... button.

    8. 在打开的对话框中点击 Add Local...(添加本地)按钮。

    9. Navigate to plugin_name/plugin_name_ios/ios/plugin_name_ios and click the Add Package button.

    10. 导航到 plugin_name/plugin_name_ios/ios/plugin_name_ios 并点击 Add Package 按钮。

    11. Ensure that it's added to the RunnerTests target and click the Add Package button.

    12. 确保已添加到 RunnerTests target 并点击 Add Package 按钮。

  15. Ensure tests pass Product > Test.

  16. 确保 Product > Test(测试)通过。