一、环境

Mac OS:10.14.6
Xcode:11.3
Flutter:1.12.13+hotfix.5 • channel stable • Dart 2.7.0
已经安装有Cocoapods,如果没有安装请移驾这里

如果集成方式有更新,请看最新官方文档

Demo地址

Note: 下载好demo后,请在kk_flutter项目中分别运行flutter pub getflutter build ios --debug后,再在Xcode上运行项目。否则可能会遇见Command PhaseScriptExecution failed with a nonzero exit code错误,或者Flutter/Flutter.h' file not found错误

Note:应用程序将无法在Release模式下运行到模拟器上,因为Flutter尚不支持Dart代码的输出x86预编译(AOT)二进制文件。 您可以在模拟器或真实设备上以Debug模式运行,而在真实设备上以Release模式运行。

二、集成方式

官方说有两种集成方式,分别是:

  1. 使用CocoaPods依赖性管理器和已安装的Flutter SDK(官方推荐)。
  2. 为Flutter引擎,已编译的Dart代码和所有Flutter插件创建Framework。手动嵌入Framework,并在Xcode中更新现有应用程序的构建设置。

三、创建Flutter_Module

  1. 进入到和我们iOS项目同级别的文件夹目录下
KKdeMacBook-Pro:~ kaye$ cd Desktop/NativeFultter/
  1. 创建flutter_module,这里将flutter_module命名为“kk_flutter”
    Note:flutter_module的命名要符合dart package name规范,要以小写单词和_相连,否则创建不成功

先给一个不规范的例子:

Last login: Wed Dec 25 09:05:21 on console
KKdeMacBook-Pro:~ kaye$ cd Desktop/NativeFultter/
KKdeMacBook-Pro:NativeFultter kaye$ flutter create -t module KKFlutter
╔════════════════════════════════════════════════════════════════════════════╗
  ║                 Welcome to Flutter! - https://flutter.dev                  ║
  ║                                                                            ║
  ║ The Flutter tool uses Google Analytics to anonymously report feature usage ║
  ║ statistics and basic crash reports. This data is used to help improve      ║
  ║ Flutter tools over time.                                                   ║
  ║                                                                            ║
  ║ Flutter tool analytics are not sent on the very first run. To disable      ║
  ║ reporting, type 'flutter config --no-analytics'. To display the current    ║
  ║ setting, type 'flutter config'. If you opt out of analytics, an opt-out    ║
  ║ event will be sent, and then no further information will be sent by the    ║
  ║ Flutter tool.                                                              ║
  ║                                                                            ║
  ║ By downloading the Flutter SDK, you agree to the Google Terms of Service.  ║
  ║ Note: The Google Privacy Policy describes how data is handled in this      ║
  ║ service.                                                                   ║
  ║                                                                            ║
  ║ Moreover, Flutter includes the Dart SDK, which may send usage metrics and  ║
  ║ crash reports to Google.                                                   ║
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
  ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting        ║
  ║                                                                            ║
  ║ See Google's privacy policy:                                               ║
  ║ https://www.google.com/intl/en/policies/privacy/                           ║
  ╚════════════════════════════════════════════════════════════════════════════╝

"KKFlutter" is not a valid Dart package name.

From the [Pubspec format
description](https://www.dartlang.org/tools/pub/pubspec.html):

**DO** use `lowercase_with_underscores` for package names.

Package names should be all lowercase, with underscores to separate words,
`just_like_this`.  Use only basic Latin letters and Arabic digits: [a-z0-9_].
Also, make sure the name is a valid Dart identifier -- that it doesn't start
with digits and isn't a reserved word.

然后再来个正常的流程:

KKdeMacBook-Pro:NativeFultter kaye$ flutter create -t module kk_flutter
Creating project kk_flutter... androidx: true
  kk_flutter/test/widget_test.dart (created)
  kk_flutter/kk_flutter.iml (created)
  kk_flutter/.gitignore (created)
  kk_flutter/.metadata (created)
  kk_flutter/pubspec.yaml (created)
  kk_flutter/README.md (created)
  kk_flutter/lib/main.dart (created)
  kk_flutter/kk_flutter_android.iml (created)
  kk_flutter/.idea/libraries/Flutter_for_Android.xml (created)
  kk_flutter/.idea/libraries/Dart_SDK.xml (created)
  kk_flutter/.idea/modules.xml (created)
  kk_flutter/.idea/workspace.xml (created)
Running "flutter pub get" in kk_flutter...                          2.9s
Wrote 12 files.

All done!
Your module code is in kk_flutter/lib/main.dart.
KKdeMacBook-Pro:NativeFultter kaye$ 

这个时候,我们的目录结构是这样的:

frc a6b482ff582377887cc833e24128c1f4 - Flutter-已有iOS工程中加入Flutter之module方式集成
创建flutter_module后的目录结构.png

进入到kk_flutter中,目录结构类似于这样:

kk_flutter/
├─.ios/ #隐藏文件,可以使用Command+shift+. 显示隐藏文件
│ ├─Runner.xcworkspace
│ └─Flutter/podhelper.rb
├─lib/ #我们的代码都写在这个文件夹中
│ └─main.dart
├─test/
└─pubspec.yaml #flutter依赖的库,都写在这里
frc ad7a948e8fd75344e0f7bb32a620d99f - Flutter-已有iOS工程中加入Flutter之module方式集成
flutter_module目录.png

四、将已经创建的flutter_module集成到现有的iOS项目中

4.1 手动导入

如果你想直接手动形式集成Framewok,可以在kk_flutter路径下,使用flutter build ios-framework --output=你想要导出的Framework路径(比如: /Users/kaye/Desktop/ios_flutter/NativeFlutter_module/Flutterframeworks),此时在你的指定目录下,会自动生成三个文件件分别是Debug、Profile、Release,三个文件夹中分别有App.frameworkFlutter.frameworkFlutterPluginRegistrant.framework等多个Framework,命令行界面如下:

KKdeMacBook-Pro:kk_flutter kaye$ flutter build ios-framework --output=/Users/kaye/Desktop/ios_flutter/NativeFlutter_module/Flutterframeworks 
Building framework for com.example.kkFlutter in debug mode...
 ├─Populating Flutter.framework...                                 172ms
 ├─Add placeholder App.framework for debug...                      177ms
 ├─Assembling Flutter resources for App.framework...                6.0s
 ├─Building plugins...                                              3.2s
 └─Moving to ../NativeFlutter_module/Flutterframeworks/Debug         0.0s
Building framework for com.example.kkFlutter in profile mode...
 ├─Populating Flutter.framework...                                 124ms
 ├─Building Dart AOT for App.framework...                          20.0s
 ├─Assembling Flutter resources for App.framework...                0.1s
 ├─Building plugins...                                             10.0s
 └─Moving to ../NativeFlutter_module/Flutterframeworks/Profile         0.0s
Building framework for com.example.kkFlutter in release mode...
 ├─Populating Flutter.framework...                                 672ms
 ├─Building Dart AOT for App.framework...                          67.0s
 ├─Assembling Flutter resources for App.framework...                0.1s
 ├─Building plugins...                                              7.9s
 └─Moving to ../NativeFlutter_module/Flutterframeworks/Release         0.0s
Frameworks written to /Users/kaye/Desktop/ios_flutter/NativeFlutter_module/Flutterframeworks.
frc 75980c9fa0221bd0fcf8d1697ba703a7 - Flutter-已有iOS工程中加入Flutter之module方式集成
编译后生成的Framework.png

将生成的三种模式下的Framework拖拽到项目中,并修改Xcode如下配置:

frc d4aa7c91e3cf770296b71e564e477942 - Flutter-已有iOS工程中加入Flutter之module方式集成
导入flutterFramework.png

然后就可以在Xcode运行了。

4.2 podfile导入

首先,此方法要求在您的项目上工作的每个开发人员都必须具有本地安装的Flutter SDK版本。 只需在Xcode中构建应用程序即可自动运行脚本以嵌入Dart和插件代码。 这允许使用Flutter模块的最新版本进行快速迭代,而无需在Xcode之外运行其他命令。

其次,你的原生项目要有Podfile文件,如果没有你可以通过命令行,进入到原生应用目录中,然后创建Podfile:

KKdeMacBook-Pro:~ kaye$ cd Desktop/NativeFultter/
KKdeMacBook-Pro:NativeFultter kaye$ ls
NativeFlutter_module    kk_flutter
KKdeMacBook-Pro:NativeFultter kaye$ cd NativeFlutter_module/
KKdeMacBook-Pro:NativeFlutter_module kaye$ ls
NativeFlutter_module        NativeFlutter_moduleTests
NativeFlutter_module.xcodeproj  NativeFlutter_moduleUITests
KKdeMacBook-Pro:NativeFlutter_module kaye$ pod init

目前我们的现有应用程序和flutter_module位于同级目录中。 如果您使用其他目录结构,则可能需要调整相对路径。

some/path/
├── kk_flutter/ #flutter_module
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
└── NativeFlutter_module/ #原生工程
    └── Podfile
  1. 打开我们刚才在原生项目中创建的Podfile文件,将下面两句代码添加到里面:
flutter_application_path = '../kk_flutter' #这里是我们创建的flutter_module的路径
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  1. 对于需要嵌入Flutter的每个Podfile目标,请调用install_all_flutter_pods(flutter_application_path)。整个Podfile文件内容如下:
# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'

flutter_application_path = '../kk_flutter' #这里是我们创建的flutter_module的路径
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'NativeFlutter_module' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for NativeFlutter_module

  install_all_flutter_pods(flutter_application_path)

  target 'NativeFlutter_moduleTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'NativeFlutter_moduleUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end
  1. 运行pod install
KKdeMacBook-Pro:NativeFlutter_module kaye$ pod install
Analyzing dependencies
Fetching podspec for `Flutter` from `../kk_flutter/.ios/Flutter/engine`
Fetching podspec for `FlutterPluginRegistrant` from `../kk_flutter/.ios/Flutter/FlutterPluginRegistrant`
Fetching podspec for `kk_flutter` from `../kk_flutter/.ios/Flutter`
Downloading dependencies
Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Installing kk_flutter (0.0.1)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `NativeFlutter_module.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There are 3 dependencies from the Podfile and 3 total pods installed.
KKdeMacBook-Pro:NativeFlutter_module kaye$ 

Note:当您在kk_flutter/pubspec.yaml中更改Flutter插件的依赖性时,请在flutter_module目录中运flutter pub get来刷新podhelper.rb脚本读取的插件列表。 然后,从您的应用程序的目录下需要再次运行pod install
podhelper.rb脚本将您的插件Flutter.frameworkApp.framework嵌入到您的项目中。
Flutter.framework是Flutter引擎的捆绑软件,而App.framework是该项目的已编译Dart代码。

  1. 打开生成的.workspace文件,Command+B,进行build一下,发现貌似没有什么问题。
    我们再来看一下目前的Xcode目录:
    frc c716bfd1f7eaeec48154ba12d8ba629c - Flutter-已有iOS工程中加入Flutter之module方式集成
    pod中生成的Frameworks.png

五、开始使用,启动FlutterEngine和FlutterViewController

要从iOS应用中启动一个Flutter界面,我们必须先创建FlutterEngine和FlutterViewController。

FlutterEngine充当Dart VM和Flutter运行时的主机,FlutterViewController依附于FlutterEngine,以将UIKit输入事件传递到Flutter中并显示FlutterEngine渲染的帧。

FlutterEngine的生存时间大于等于FlutterViewController的生存时间。

通常建议为应用程序预先创建一个长生命周期的FlutterEngine,因为:
显示FlutterViewController时,第一帧显示会更快。
您的Flutter和Dart状态将超过一个FlutterViewController的生存时间。
在显示UI之前,您的应用程序和插件可以与Flutter和Dart逻辑进行交互。

5.1 创建FlutterEngine

考虑到上面所说,预先创建FlutterEngine,我们先在AppDelegate.h中创建一个引擎,并且暴露出来一个属性。然后在AppDelegate.m中注册引擎。

// Appdelegate.h
#import 
@import Flutter; // 导入Flutter

@interface AppDelegate : FlutterAppDelegate //UIResponder 

//@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, strong) FlutterEngine *flutterEngine;

@end

// AppDelegate.m
#import "AppDelegate.h"
#import  //Used to connect plugins.

@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"MyFlutterEngine"];
    // 使用默认的Flutter路由运行默认的Dart入口点。
    // 当在AppDelegate中创建的FlutterEngine上调用run时,默认Dart库的默认main()入口点函数将运行。
    [self.flutterEngine run];
    [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
    
    return [super application:application didFinishLaunchingWithOptions:launchOptions];;
}

5.2 创建FlutterViewController

进入我们原生iOS Demo工程中,打开ViewController.h,写入如下代码,测试我们原生打开FlutterViewController。

#import "ViewController.h"
#import "AppDelegate.h"
@import Flutter;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    UIButton *flutterButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 200, 120, 40)];
    [flutterButton setTitle:@"show flutter" forState:UIControlStateNormal];
    [flutterButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [flutterButton addTarget:self action:@selector(flutterButtonAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:flutterButton];
    
    [self.navigationController setNavigationBarHidden:YES];
}

- (void)flutterButtonAction {
    // 获取引擎
    FlutterEngine *flutterEngine = [(AppDelegate *)[UIApplication sharedApplication].delegate flutterEngine];
    // 创建FlutterViewController
    FlutterViewController *flutterVC = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
    // 跳转
    [self.navigationController pushViewController:flutterVC animated:YES];
    // 当然你也可以使用下面的方法跳转
    //[self presentViewController:flutterVC animated:YES completion:nil];
}

至此,我们就已经可以完成原生集成Flutter,并且完成跳转的工作了,效果如下:

frc 44979c36d59628125151d89962e6ffbf - Flutter-已有iOS工程中加入Flutter之module方式集成
效果图.gif

另外,官方还给了另外一种使用隐式FlutterEngine的方式创建FlutterViewController,这种方式,我们不需要预先创建FlutterEngine,而去按需创建,尤其是我们Flutter界面很少,而且不知道什么时候会启动Flutter界面的时候,会合适些,但是也牺牲了一些,按照官方说的就是在首次显示FlutterUI的时候,会有些延迟,所以官方不是很推荐隐式创建FlutterEngine的方式。具体代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    UIButton *flutterButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 200, 120, 40)];
    [flutterButton setTitle:@"show flutter" forState:UIControlStateNormal];
    [flutterButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [flutterButton addTarget:self action:@selector(flutterButtonAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:flutterButton];
    
    UIButton *flutterButton2 = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 200, 40)];
    [flutterButton2 setTitle:@"隐式创建引擎" forState:UIControlStateNormal];
    [flutterButton2 setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [flutterButton2 addTarget:self action:@selector(createFlutterViewControllerWithImplicitFlutterEngine) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:flutterButton2];
    
    [self.navigationController setNavigationBarHidden:YES];
}

- (void)createFlutterViewControllerWithImplicitFlutterEngine {
    // 创建FlutterViewController
    FlutterViewController *flutterVC = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
    // 跳转
    [self.navigationController pushViewController:flutterVC animated:YES];
}

六、相关内容的一些说明

6.1 FlutterAppDelegate

有的应用可能不能像我们的demo中那样,直接让AppDelegate继承自FlutterAppDelegate,这种方法是官方推荐,好处就是可以监听到诸如点击状态栏回到顶部此类操作。
但是这并不是强制的,我们在不能直接继承的情况下,为了能够让我们的Flutter能够响应一部分的App生命周期事件,我们可以在AppDelegate.h中遵循FlutterAppLifeCycleProvider,并在AppDelegate.m中实现代理,以确保能够监听到相应的事件行为:

// AppDelegate2.h
@import Flutter;
@import UIKit;
@import FlutterPluginRegistrant;

@interface AppDelegate : UIResponder 
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

// AppDelegate2.m
#import "AppDelegate2.h"

@interface AppDelegate2 ()

@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;

@end

@implementation AppDelegate2

- (instancetype)init {
    if (self = [super init]) {
        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
    }
    return self;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"MyFlutterEngine"];
    // 使用默认的Flutter路由运行默认的Dart入口点。
    // 当在AppDelegate中创建的FlutterEngine上调用run时,默认Dart库的默认main()入口点函数将运行。
    [self.flutterEngine run];
    [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
    
    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}

// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
    if ([viewController isKindOfClass:[FlutterViewController class]]) {
        return (FlutterViewController*)viewController;
    }
    return nil;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    [super touchesBegan:touches withEvent:event];
    
    // Pass status bar taps to key window Flutter rootViewController.
    if (self.rootFlutterViewController != nil) {
        [self.rootFlutterViewController handleStatusBarTouches:event];
    }
}
//....
// 这里是生命周期代码,具体的可以参考Demo中AppDelegate2.m
@end

6.2 Dart 入口(Dart EntryPoint)

FlutterEngine调用run方法,默认情况下,运行的是lib/main.dart文件中的main()入口,我们也可以运行不同的入口,通过使用runWithEntrypoint,传入一个字符串参数,以使用其他的dart入口(入口在lib/main.dart文件中),但是请注意,除main()之外的Dart入口点函数必须使用以下注释:

// flutter
@pragma('vm:entry-point')
void myOtherEntryPoint() { ... };

// oc
[self.flutterEngine runWithEntrypoint:@"myOtherEntryPoint"];

Flutter-已有iOS工程中加入Flutter之module方式集成