简介
本文记录我在 2023.09 对将 Flutter module 集成到 iOS项目的探索过程,结合中文官方文档及网络搜索的结果后的实践内容。共有四种方法,包含官方文档的三种和开发者们扩展出的第四种。
系统要求
你的开发环境必须满足 Flutter 对 macOS 系统的版本要求 并 已经安装 Xcode,Flutter 支持 iOS 11 及以上。此外,你还需要 1.10 或以上版本的 CocoaPods。
准备
由于要进行多种方式测试,避免混乱,先创建一个父文件夹 iOS_flutter_mix ,再建四个子文件夹 a、b、c、d 分别进行测试,不至于在同一个项目上测试出现混乱,或到最后只能看到最终测试的结果,前边的都被覆盖了。
开始实验
方式 A,使用 CocoaPods 和 Flutter SDK 集成
这个方式需要项目的所有开发者本地都有 Flutter 环境。你的工程在每次构建的的时候,都将会从源码里编译 Flutter 模块。
- 在 a 文件夹下创建 iOS 工程和 Flutter module,使用命令行或者IDE操作都可以
- 在 iOS 工程下创建 Podfile 文件
cd iOS_flutter_mix/a/MyApp
pod init
此时路径目录如下:
a
├── MyApp
│ ├── MyApp
│ ├── MyApp.xcodeproj
│ ├── MyAppTests
│ ├── MyAppUITests
│ └── Podfile
└── my_flutter
├── .android
├── .dart_tool
├── .gitignore
├── .idea
├── .ios
├── .metadata
├── README.md
├── analysis_options.yaml
├── lib
├── my_flutter.iml
├── my_flutter_android.iml
├── pubspec.lock
├── pubspec.yaml
└── test
- 在
Podfile中添加下面代码:
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
- 每个需要集成 Flutter 的 Podfile target,执行
install_all_flutter_pods(flutter_application_path):
target 'MyApp' do
install_all_flutter_pods(flutter_application_path)
end
- 在
Podfile的post_install部分调用flutter_post_install(installer)。
post_install do |installer|
flutter_post_install(installer) if defined?(flutter_post_install)
end
此时 Podfile 文件内容如下
# Uncomment the next line to define a global platform for your project
platform :ios, '12.0'
flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
target 'MyApp' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
install_all_flutter_pods(flutter_application_path)
post_install do |installer|
flutter_post_install(installer) if defined?(flutter_post_install)
end
end
flutter_post_install 方法(Flutter 3.1.0 中新增的)增加了原生 Apple Silicon arm64 iOS 模拟器的支持。它包括 if defined?(flutter_post_install) 的检查以确保你的 Podfile 在旧版本的没有该方法的 Flutter 上也能正常运行。
- 运行
pod install。Flutter module 就会通过 pod 链接到 iOS 工程中,如图:
当你在 my_flutter/pubspec.yaml 改变了 Flutter plugin 依赖,需要在 Flutter module 目录运行 flutter pub get,来更新会被podhelper.rb 脚本用到的 plugin 列表,然后再次在你的应用目录 /MyApp 运行 pod install.
- 打开/MyApp/MyApp.xcworkspace,在iOS项目中测试效果,编写如下代码(非官方推荐标准写法,只为测试是否集成成功)
方式 B,在 Xcode 中集成 frameworks
除了上面的方法,你也可以创建必备的 frameworks,手动修改既有 Xcode 项目,将他们集成进去。当你组内其它成员们不能在本地安装 Flutter SDK 和 CocoaPods,或者你不想使用 CocoaPods 作为既有应用的依赖管理时,这种方法会比较合适。但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework。
- 在 b 文件夹下分别创建 iOS 工程、Flutter module,再在 iOS 工程目录下加一个 flutter_lib 文件夹备用。 目录如下所示:
.
├── MyApp
│ ├── MyApp
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── Base.lproj
│ │ ├── Info.plist
│ │ ├── SceneDelegate.swift
│ │ └── ViewController.swift
│ ├── MyApp.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ └── xcuserdata
│ ├── MyAppTests
│ │ └── MyAppTests.swift
│ ├── MyAppUITests
│ │ ├── MyAppUITests.swift
│ │ └── MyAppUITestsLaunchTests.swift
│ └── flutter_lib
└── my_flutter
├── README.md
├── analysis_options.yaml
├── build
│ └── ios
├── lib
│ └── main.dart
├── my_flutter.iml
├── my_flutter_android.iml
├── pubspec.lock
├── pubspec.yaml
└── test
└── widget_test.dart
- 在 Flutter module 目录下运行
flutter build ios-framework
cd iOS_flutter_mix/b/my_flutter
flutter build ios-framework
编译产物在/b/my_flutter/build/ios/framework中,也可以使用 flutter build ios-framework --output=b/MyApp/flutter_lib/ 把编译产物输出到指定文件夹
flutter build ios-framework --output=../MyApp/flutter_lib/
- 在 Xcode 中将生成的 frameworks 集成到你的既有应用中。例如,你可以在
some/path/MyApp/Flutter/Debug/目录拖拽 frameworks 到你的应用 target 编译设置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中选择 “Embed & Sign”。 - 打开/MyApp/MyApp.xcodeproj,编写如方式 A 中的代码运行,会看到一样的效果。
- 由于编译 flutter 得到的产物既有动态框架,也有静态框架,这两种集成到 iOS 项目中有不同的方式,具体可查看官方文档选择链接到框架 或者 内嵌框架。
方式 C,使用 CocoaPods 在 Xcode 和 Flutter 框架中内嵌应用和插件框架
除了将一个很大的 Flutter.framework 分发给其他开发者、机器或者持续集成 (CI) 系统之外,你可以加入一个参数 --cocoapods 将 Flutter 框架作为一个 CocoaPods 的 podspec 文件分发。这将会生成一个 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件。
- 在 c 文件夹下分别创建 iOS 工程、Flutter module,再在 iOS 工程目录下加一个 flutter_lib 文件夹备用。
.
├── MyApp
│ ├── MyApp
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── Base.lproj
│ │ ├── Info.plist
│ │ ├── SceneDelegate.swift
│ │ └── ViewController.swift
│ ├── MyApp.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ └── xcuserdata
│ ├── MyAppTests
│ │ └── MyAppTests.swift
│ ├── MyAppUITests
│ │ ├── MyAppUITests.swift
│ │ └── MyAppUITestsLaunchTests.swift
│ └── flutter_lib
└── my_flutter
├── .android
│ ├── Flutter
│ ├── app
│ ├── build.gradle
│ ├── gradle
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── include_flutter.groovy
│ ├── local.properties
│ ├── settings.gradle
│ └── src
├── .dart_tool
│ ├── package_config.json
│ ├── package_config_subset
│ └── version
├── .gitignore
├── .idea
│ ├── libraries
│ ├── modules.xml
│ └── workspace.xml
├── .ios
│ ├── Config
│ ├── Flutter
│ ├── Runner
│ ├── Runner.xcodeproj
│ └── Runner.xcworkspace
├── .metadata
├── README.md
├── analysis_options.yaml
├── lib
│ └── main.dart
├── my_flutter.iml
├── my_flutter_android.iml
├── pubspec.lock
├── pubspec.yaml
└── test
└── widget_test.dart
- 这次为了演示与之前不同的情况,在 flutter 的
pubspec.yaml中添加其他三方库的引用。
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
# 添加数据持久化插件 https://pub.flutter-io.cn/packages/shared_preferences
shared_preferences: ^0.5.4+3
然后执行 pub get。
3. 要生成 Flutter.podspec 和框架,命令行切换到 Flutter module 根目录,然后运行以下命令:
cd iOS_flutter_mix/c/my_flutter
flutter build ios-framework --cocoapods --output=../MyApp/flutter_lib/
此时 flutter_lib 目录如下:
.
├── Debug
│ ├── App.xcframework
│ ├── Flutter.podspec
│ ├── FlutterPluginRegistrant.xcframework(只有当你有带有iOS平台代码的插件时)
│ └── shared_preferences.xcframework(每个插件都是独立的框架)
├── Profile
│ ├── App.xcframework
│ ├── Flutter.podspec
│ ├── FlutterPluginRegistrant.xcframework
│ └── shared_preferences.xcframework
└── Release
├── App.xcframework
├── Flutter.podspec
├── FlutterPluginRegistrant.xcframework
└── shared_preferences.xcframework
shared_preferences.xcframework 是 flutter 对应其他平台的数据存储插件,例如 iOS 中的 NSUserDefaults
FlutterPluginRegistrant.xcframework 是当 flutter 中有与 iOS 平台相关的插件时生成的,用于在 flutter 与 iOS 平台交互时使用。
- 创建 pod 并添加依赖,使用 CocoaPods 的宿主应用程序可以将 Flutter 添加到 Podfile 中,
cd MyApp
pod init
pod 'Flutter', :podspec => './flutter_lib/[build mode]/Flutter.podspec'
你必须选择相应的构建模式进行硬编码,将构建模式的值写在上面指令中的 [build mode] 位置。例如,你需要 flutter attach 的时候,应该使用 Debug,在你准备发布版本的时候,应该使用 Release。
然后
pod install
此时 Flutter.xcframework 已被 pod 管理,其他的框架像方式 B 那样链接并嵌入到你现有的应用程序中。
- 如上述方式在 iOS 项目中添加代码测试。
方式 D,优化改动方式 C,把所有框架都用 pod 来管理。
方式 C 中只对 Flutter.xcframework 进行 pod 管理,其他还是需要手动添加,不太方便。所以我们准备自建 pod 库,把所有框架都放进一个 pod 库中,然后添加到 iOS 项目中。
- 在 d 文件夹下创建 Flutter module、iOS 项目和 flutter_lib_pod 文件夹。
- 在 d 文件夹下执行
pod lib create flutter_lib_pod命令,然后回答几个问题,根据你需求来就行。
cd d
pod lib create flutter_lib_pod
Cloning `https://github.com/CocoaPods/pod-template.git` into `flutter_lib_pod`.
Configuring flutter_lib_pod template.
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
------------------------------
To get you started we need to ask a few questions, this should only take a minute.
If this is your first time we recommend running through with the guide:
- https://guides.cocoapods.org/making/using-pod-lib-create.html
( hold cmd and double click links to open in a browser. )
What platform do you want to use?? [ iOS / macOS ]
>
ios
What language do you want to use?? [ Swift / ObjC ]
>
swift
Would you like to include a demo application with your library? [ Yes / No ]
> no
Which testing frameworks will you use? [ Quick / None ]
> none
Would you like to do view based testing? [ Yes / No ]
> no
此时, flutter_lib_pod 文件夹如下
- 在 d/flutter_lib_pod 中新建文件夹 ios_frameworks 备用,然后来到 d/my_flutter 里创建一个
build_and_move_file.sh脚本文件,用来编译 flutter 并把编译出的框架移动到 d/flutter_lib_pod/ios_frameworks 中添加到 pod 库里。 脚本文件参考网友写的并做了修改
if [ -z $out ]; then
out='ios_frameworks'
fi
echo "准备输出所有文件到目录: $out"
echo "清除所有已编译文件"
find . -d -name build | xargs rm -rf
flutter clean
rm -rf $out
rm -rf build
flutter packages get
addFlag(){
cat .ios/Podfile > tmp1.txt
echo "use_frameworks!" >> tmp2.txt
cat tmp1.txt >> tmp2.txt
cat tmp2.txt > .ios/Podfile
rm tmp1.txt tmp2.txt
}
echo "检查 .ios/Podfile文件状态"
a=$(cat .ios/Podfile)
if [[ $a == use* ]]; then
echo '已经添加use_frameworks, 不再添加'
else
echo '未添加use_frameworks,准备添加'
addFlag
echo "添加use_frameworks 完成"
fi
echo "编译flutter"
# 得到的是融合了模拟器和真机环境的 xcframework,默认包含 Debug、Profile、Release
flutter build ios-framework
# 下方这几种编译模式得到的是 framework,模拟器和真机分开的
#flutter build ios --debug --no-codesign
#flutter build ios --simulator --no-codesign
#flutter build ios --release --no-codesign
echo "编译flutter完成"
#创建输出路径
mkdir $out
#mkdir $out/Debug
#mkdir $out/Profile
#mkdir $out/Release
cp -r build/ios/framework $out
#也可以单独拷贝
#cp -r build/ios/framework/Debug/*.xcframework $out/Debug
#cp -r build/ios/framework/Profile/*.xcframework $out/Profile
#cp -r build/ios/framework/Release/*.xcframework $out/Release
#debug
#cp -r build/ios/Debug-iphoneos/*.framework $out
#cp -r build/ios/Debug-iphoneos/*/*.framework $out
#release
#cp -r build/ios/Release-iphoneos/*/*.framework $out
#cp -r build/ios/Release-iphoneos/*.framework $out
#网友的写法是这样的,但是现在路径变动了,已经不在这儿了
#cp -r .ios/Flutter/App.framework $out
#cp -r .ios/Flutter/engine/Flutter.framework $out
echo "复制framework库到临时文件夹: $out"
libpath='../flutter_lib_pod/'
rm -rf "$libpath/ios_frameworks"
mkdir $libpath
cp -r $out $libpath
echo "复制库文件到: $libpath"
- 执行
build_and_move_file.sh脚本文件
cd /d/my_flutter
sh build_and_move_file.sh
把 d/my_flutter/build/ios/framework 中的三种模式下的 xcframework 都导入到了 d/flutter_lib_pod/ios_frameworks/framework
- 编辑
d/flutter_lib_pod/flutter_lib_pod.podspec文件,在 end 前一行添加代码,添加后整体如下(避免看着杂乱,已删除部分注释)
Pod::Spec.new do |s|
s.name = 'flutter_lib_pod'
s.version = '0.1.0'
s.summary = 'A short description of flutter_lib_pod.'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://xxx/flutter_lib_pod'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'xxx' => 'xxx' }
s.source = { :git => 'xxx/flutter_lib_pod.git', :tag => s.version.to_s }
s.ios.deployment_target = '12.0'
# 添加编译出来的依赖库
s.static_framework = true
p = Dir::open("ios_frameworks/framework")
arr = Array.new
arr.push('ios_frameworks/framework/Release/*.xcframework')
s.ios.vendored_frameworks = arr
end
注意到这里是把 Release 模式下的框架给添加进来了,为了方便区分管理,我们可以把 flutter_lib_pod.podspec 文件拷贝一份命名为 flutter_lib_pod_debug.podspec,并修改内容对应为 Debug
Pod::Spec.new do |s|
# name需要修改为flutter_lib_pod_debug
s.name = 'flutter_lib_pod_debug'
s.version = '0.1.0'
s.summary = 'A short description of flutter_lib_pod.'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://xxx/flutter_lib_pod'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'xxx' => 'xxx' }
s.source = { :git => 'https://xxx/flutter_lib_pod.git', :tag => s.version.to_s }
s.ios.deployment_target = '12.0'
# 添加编译出来的依赖库
s.static_framework = true
p = Dir::open("ios_frameworks/framework")
arr = Array.new
# 路径需要修改为 ios_frameworks/framework/Debug/*.xcframework
arr.push('ios_frameworks/framework/Debug/*.xcframework')
s.ios.vendored_frameworks = arr
end
- 在MyApp项目中创建 Podfile 文件并添加 pod 引用
cd d/MyApp
pod init
platform :ios, '12.0'
target 'MyApp' do
use_frameworks!
pod 'flutter_lib_pod_debug', :path => '../flutter_lib_pod', :configurations => ['Debug']
# pod 'flutter_lib_pod', :path => '../flutter_lib_pod', :configurations => ['Release']
end
执行
pod install
- 打开 MyApp.xcworkspace,可以观察到 框架已经导入到 iOS 项目中
- 编写测试代码,查看效果
方式 E,终极解决方案。
把这个方式单独列出来,是觉得此方案可能是最适合的。既然方式 D 已经实现了通过本地 pod 库进行管理这些 xcframework,那么把这个 pod 库关联到远端,就实现了平常使用 pod 导入三方库那样便捷了,在多人开发中也不需要所有人都安装 flutter 环境。
- 在 github、gitlib 或其他平台上创建一个空的仓库备用。
- 新建文件夹 e、把方式 D 中的 my_flutter 和 flutter_lib_pod 拷贝过来,修改 flutter_lib_pod 下的 flutter_lib_pod_debug.podspec
这里项目的信息要和远端仓库保持一致。
- 把 flutter_lib_pod 与远程库关联起来
cd flutter_lib_pod
git remote add origin https://github.com/NothingLuo/flutter_lib_pod.git
然后使用命令行或者 Sourcetree 把本地文件推送到远端,新建一个 tag 0.1.0 用于测试。
- 新建或者拷贝上边的 iOS 项目,更改 Podfile 文件。
platform :ios, '12.0'
target 'MyApp' do
use_frameworks!
pod 'flutter_lib_pod_debug', :git => 'https://github.com/NothingLuo/flutter_lib_pod.git', :configurations => ['Debug']
# pod 'flutter_lib_pod_debug', :path => '../flutter_lib_pod', :configurations => ['Debug']
# pod 'flutter_lib_pod', :path => '../flutter_lib_pod', :configurations => ['Release']
end
如果使用拷贝过来的 iOS 项目,先清理一下 Podfile,再执行 pod install 把之前导入的库清除掉。
把 pod 库的 git 地址指向远端。
5. 执行 pod install 拉取远端库。
cd MyApp
pod install
耐心等待一下,那些 xcframework 文有点大,完成后打开 MyApp.xcworkspace
6. 同上述的几种方式,添加代码测试,效果相同。
对比总结
几种方式各有优劣,选择适合自己的使用即可。
A,使用最简单省事,不需要复杂配置,方便原生与 flutter 联调,也是官方推荐的方式;缺点就是团队开发的话需要每个 iOS 开发都需要在本地安装有 flutter 环境,flutter 和 iOS 原生代码还耦合在一起。
B,不需要每个开发者本地都安装 flutter 环境,由开发 flutter 的负责编译后把 framework 分发给 iOS 开发者手动集成进入工程,也不需要依赖 Cocoapods;但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework,然后重新给 iOS 开发 framework进行替换,也要注意发版时 debug 和 release 包的替换。
C,不推荐,只是使用 Cocoapods 管理了一个 Flutter.xcframework,其他的还是需要和方式 B 一样处理。
D,此方式也不需要每个 iOS 开发本地都有 flutter 环境,两端代码之间也不耦合,就是前期配置稍微复杂一点,配置完之后就会简单很多,需要 flutter 开发向 iOS 工程中通过命令导入框架。
E,其实就是方式 D 的远程版,把本地 pod 库关联到远程库,这个可以让 flutter 开发完运行脚本打包再把框架上传到远端 pod 库,iOS 开发自己去配置 Podfile 拉取。
但是除了方式 A,其他方式两端混合联调起来都比较麻烦,毕竟这几种都是导入的是静态或动态库,只能分开调试,不过模块之间没有太多依赖也就无所谓了。
tip:由于本人技术有限,且是第一次做将 Flutter module 集成到 iOS 项目,也做了大量测试,但整理过程中难免有所疏漏或错误,请谅解,谢谢。
后续开发
本篇只讲了 Flutter 与 iOS 项目之间的各种集成方式,后续的页面开发跳转可参考官方文档:在 iOS 应用中添加 Flutter 页面 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter 或其他文章。
致谢
本文参考以下文章,🙏
将 Flutter module 集成到 iOS 项目 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter