将 Flutter module 集成到 iOS 项目

Flutter UI 组件可以渐进式地内嵌到你现有的 iOS 应用中,下面是几种方法:

  1. 使用 CocoaPods 依赖管理器安装 Flutter SDK 使用这种方法,每次构建应用的时候都会从源代码中编译 flutter_module。(推荐)

  2. 创建一个框架,把 Flutter 引擎、已编译的 Dart 代码和所有 Flutter 插件都放进去 这种方式你可以手动嵌入这个框架,并在 Xcode 中更改现有的应用的构建设置。如果不想要求开发团队的每一位成员都在本地安装 Flutter SDK 和 Cocoapods,这种方式比较适用。

  3. 为已编译的 Dart 代码和所有 Flutter 插件创建一个框架,对 Flutter 引擎使用 CocoaPods 来管理 这种方式是将应用内容和插件作为内嵌的框架,但将 Flutter 引擎作为 CocoaPods podspec 分发。这有点类似第二种方式,但是它为分发大型的 Flutter.xcframework 文件提供了替代方案。

例如使用 UIKit 构建的应用,请参阅 add_to_app 代码示例 中 iOS 这个目录。有关使用 SwiftUI 的示例,请参阅 News Feed App 中的 iOS 目录。

系统要求

你的开发环境必须满足 Flutter 对 macOS 系统的版本要求已经安装 Xcode,Flutter 支持 iOS 12 及以上。此外,你还需要 1.10 或以上版本的 CocoaPods

创建 Flutter module

为了将 Flutter 集成到你的既有应用里,参考上面的任意方法先创建一个 Flutter module。

在命令行中执行:

cd some/path/
flutter create --template module my_flutter

Flutter module 会创建在 some/path/my_flutter/ 目录。如果你使用上述第一种方法,则应在与现有 iOS 应用工程的父目录中创建这个 Flutter module。

在这个目录中,你可以像在其它 Flutter 项目中一样,执行 flutter 命令。比如 flutter run --debug 或者 flutter build ios。同样,你也可以通过 Android Studio/IntelliJ 或者 VS Code 中的 Flutter 和 Dart 插件运行这个 module,在集成到现有应用前,这个项目在 Flutter module 中包含了一个单视图的示例代码,对 Flutter 侧代码的测试会有帮助。

Module 的目录结构

my_flutter module 里,目录结构和普通 Flutter 应用类似:

my_flutter/
├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml

添加你的 Dart 代码到 lib/ 目录。

添加 Flutter 依赖到 my_flutter/pubspec.yaml,包括 Flutter packages 和 plugins。

.ios/ 隐藏文件夹包含了一个 Xcode workspace,用于单独运行你的 Flutter module。它是一个独立启动 Flutter 代码的壳工程,并且包含了一个帮助脚本,用于编译 framewroks 或者使用 CocoaPods 将 Flutter module 集成到你的既有应用。

在你的既有应用中集成 Flutter module

在你的 module 开发完成后,你就能使用页面顶部描述的方法将其嵌入到应用中去了。

使用 Flutter 会 增加应用体积

选项 A - 使用 CocoaPods 和 Flutter SDK 集成

这个方法需要你的项目的所有开发者,都在本地安装 Flutter SDK。你的工程在每次构建的的时候,都将会从源码里编译 Flutter 模块。只需要在 Xcode 中编译应用,就可以自动运行脚本来集成 Dart 代码和插件。这个方法允许你使用 Flutter module 中的最新代码快速迭代开发,而无需在 Xcode 以外执行额外的命令。

下面的示例假设你的既有应用和 Flutter module 在相邻目录。如果你有不同的目录结构,需要适配到对应的路径。

some/path/
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
└── MyApp/
    └── Podfile

如果你的应用下(MyApp)还没有 Podfile,请运行 pod init 来创建一个。你可以在 CocoaPods 起步指南 中了解更多。

  1. Podfile 中添加下面代码:

    MyApp/Podfile
    flutter_application_path = '../my_flutter'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  2. 每个需要集成 Flutter 的 Podfile target,执行 install_all_flutter_pods(flutter_application_path)

    MyApp/Podfile
    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
  3. Podfilepost_install 部分,调用 flutter_post_install(installer)

    MyApp/Podfile
    post_install do |installer|
      flutter_post_install(installer) if defined?(flutter_post_install)
    end
  4. 运行 pod install

podhelper.rb 脚本会把你的 plugins, Flutter.framework,和 App.framework 集成到你的项目中。

你应用的 Debug 和 Release 编译配置,将会集成相对应的 Debug 或 Release 的 编译产物。可以增加一个 Profile 编译配置用于在 profile 模式下测试应用。

在 Xcode 中打开 MyApp.xcworkspace ,你现在可以使用 ⌘B 编译项目了。

选项 B - 在 Xcode 中集成 frameworks

除了上面的方法,你也可以创建必备的 frameworks,手动修改既有 Xcode 项目,将他们集成进去。当你组内其它成员们不能在本地安装 Flutter SDK 和 CocoaPods,或者你不想使用 CocoaPods 作为既有应用的依赖管理时,这种方法会比较合适。但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework

下面的示例假设你想在 some/path/MyApp/Flutter/ 目录下创建 frameworks:

flutter build ios-framework --output=some/path/MyApp/Flutter/
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.xcframework
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework (only if you have plugins with iOS platform code)
    │   └── example_plugin.xcframework (each plugin is a separate framework)
    ├── Profile/
    │   ├── Flutter.xcframework
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework
    └── Release/
        ├── Flutter.xcframework
        ├── App.xcframework
        ├── FlutterPluginRegistrant.xcframework
        └── example_plugin.xcframework

在 Xcode 中将生成的 frameworks 集成到你的既有应用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目录拖拽 frameworks 到你的应用 target 编译设置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中选择 “Embed & Sign”。

例如,你可以将框架从 Finder 的 some/path/MyApp/Flutter/Release/ 拖到你的目标项目中,然后点击以下步骤 build settings > Build Phases > Link Binary With Libraries。

在 target 的编译设置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/

Update Framework Search Paths in Xcode

Embed the frameworks

内嵌框架

生成的动态框架必须嵌入你的应用并且在运行时被加载。

例如,你可以从应用框架组中拖拽框架(除了 FlutterPluginRegistrant 以及其他的静态框架)到你的目标 ‘ build settings > Build Phases > Embed Frameworks。然后从下拉菜单中选择 “Embed & Sign”。

之后它们将出现在 Build Phases 中的 Embed Frameworks 内,如下所示:

Embed frameworks in Xcode

你现在可以在 Xcode中使用 ⌘B 编译项目。

选项 C - 使用 CocoaPods 在 Xcode 和 Flutter 框架中内嵌应用和插件框架

除了将一个很大的 Flutter.framework 分发给其他开发者、机器或者持续集成 (CI) 系统之外,你可以加入一个参数 --cocoapods 将 Flutter 框架作为一个 CocoaPods 的 podspec 文件分发。这将会生成一个 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件。如选项 B 中所说的那样,它将会生成 App.framework 和插件框架。

要生成 Flutter.podspec 和框架,命令行切换到 Flutter module 根目录,然后运行以下命令:

flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/
some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.podspec
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework (each plugin with iOS platform code is a separate framework)
    ├── Profile/
    │   ├── Flutter.podspec
    │   ├── App.xcframework
    │   ├── FlutterPluginRegistrant.xcframework
    │   └── example_plugin.xcframework
    └── Release/
        ├── Flutter.podspec
        ├── App.xcframework
        ├── FlutterPluginRegistrant.xcframework
        └── example_plugin.xcframework

使用 CocoaPods 的宿主应用程序可以将 Flutter 添加到 Podfile 中:

MyApp/Podfile
pod 'Flutter', :podspec => 'some/path/MyApp/Flutter/[build mode]/Flutter.podspec'

如选项 B 所述,将生成的 App.xcframework、 FlutterPluginRegistrant.xcframework 以及任何插件框架,链接并嵌入到你现有的应用程序中。

本地网络隐私权限

在 iOS 14 及更高的版本中,可以在应用程序的 Debug 版本中启用 Dart 的多播 DNS 服务 (multicast DNS service),通过 flutter attach 添加 调试功能,如热重载和 DevTools

还有一种方式是将每种不同的构建配置,单独创建对应配置的 Info.plist。下面的说明假定默认为 调试版本 (Debug)发布版本 (Release)。根据应用程序构建配置的需要调整名称。

  1. 将应用程序中的 Info.plist 重命名为 Info-Debug.plist,再复制一个相同的文件并重命名为 Info-Release.plist,最后将 Info-Debug.plistInfo-Release.plist 添加到 Xcode 项目中。

    Info-Debug.plist and Info-Release.plist in Xcode
  2. Info-Debug.plist 添加 key NSBonjourServices,并将它的值设置为数组 (Array),然后在该数组中添加 _dartVmService._tcp 字符串 (String)。

    可以选择添加 key NSLocalNetworkUsageDescription,并设置为你自定义的权限提示对话框文本。

    Info-Debug.plist with additional keys
  3. 在 target 构建设置中,将 Info.plist File (INFOPLIST_FILE) 设置路径从 path/to/Info.plist 改为 path/to/Info-$(CONFIGURATION).plist

    Set INFOPLIST_FILE build setting

    这个设置将会在 Debug 时,使用 Info-Debug.plist 的配置,在 Release 时,使用 Info-Release.plist 的配置。

    Resolved INFOPLIST_FILE build setting

    又或者,你可以明确地将 Debug 的路径设置为 Info-Debug.plist,将 Release 的路径设置为 Info-Release.plist

  4. 如果 Info-Release.plist 在 target 中 Build Settings > Build Phases > Copy Bundle Resources 的时候,请删除它。

    Copy Bundle build phase

    现在 Debug 应用程序会在 Flutter 启动时提示本地网络权限。也可以通过打开 设置 > 隐私与安全性 > 本地网络 > 你的应用程序 来允许该权限。

    Local network permission dialog

Apple Silicon (arm64 Macs)

在使用 Apple Silicon 芯片的 Mac 上 (M1),宿主应用将针对 arm64 架构的模拟器编译。尽管 Flutter 支持 arm64 的 iOS 模拟器,但一些插件仍有可能未进行支持。当你在使用这些插件时,你会遇到 Undefined symbols for architecture arm64 的错误,此时你必须从模拟器支持架构中移除 arm64

在宿主应用的 Target 中,找到名为 Excluded Architectures (EXCLUDED_ARCHS) 的构建设置。单击右侧的箭头指示器图标以展开可用的构建配置。将鼠标悬停在 Debug 处并单击加号图标。将 Any SDK 更改为 Any iOS Simulator SDK。然后向构建设置值中添加 arm64

Set conditional EXCLUDED_ARCHS build setting

当全部都正确设置后,Xcode 将会向你的 project.pbxproj 文件中添加 "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;

然后对全部 iOS 目标再次执行单元测试。

开发

你现在可以 添加一个 Flutter 页面 到你的既有应用中。