IOS使用Flutter模块

1,264 阅读6分钟

往期文章

寻找IOS相册中相似图片
NSNotification与类对象,实例对象
iCloud-Documents存储
CocoaPods私有源搭建
Swarm区块链分布式存储使用
MacOS流编辑器sed

创建Flutter模块

生成模板工程

首先切换到一个存放Flutter模块的目录。在使用命令flutter create --template module moduleName创建一个模板工程

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

moduleName是我们生成模块的名称。

image.png

命令执行完毕成功创建了一个my_flutter的模板工程

模板工程结构

模板工程创建完毕之后,我们来看看模板工程的结构。

image.png

可以看到flutter模块目录与flutter工程目录结构差不多相同,各个目录的作用如下

目录作用
.android存放安卓工程目录(用于单独运行flutter模块壳工程)
.ios存放ios工程目录(用于单独运行flutter模块壳工程)
lib存放flutter源代码
pubspec.yamlflutter配置文件

.ios目录和ios目录

从模板结构中我们可以看到,没有android 和ios目录。只有.ios和.android目录。那.ios目录和ios目录的区别是什么呢?

.ios目录结构

image.png

ios目录结构

image.png

在flutter工程中,ios目录存放的是flutter工程根据ios平台编译的产物,用于发布flutter应用(ios端)或者使用xcode中的特定功能(例如:开启推送,icloud容器,定位,权限等等问题)

在flutter模块工程中,.ios目录是用于单独运行flutter模块的壳工程,比方说你的flutter模块更新的功能,但你又不想重新构建整个原生工程,那么你就可以使用这个壳工程来运行测试你的flutter模块。

此外.ios目录中有App.framework,这里面存放着我们flutter模块中的dart源代码编译后的产物

FlutterPluginRegistrant目录用于,存放我们的flutter第三方插件

还有engin目录,用于在ios环境中运行我们的flutter模块。

以及flutter模块安装帮组脚本podhelper.rb

注意 ios平台的代码是写在我们的ios原生工程中,而不是.ios目录下的flutter模块的壳工程中。在.ios目录下的flutter模块壳工程,有可能被flutter重写,并且不会集成到你现有的xcode工程中,不要放ios代码不要放ios代码不要放ios代码

安装Flutter模块到Xcode工程中

CocoaPod安装

首先创建一个Podfile

cd path
vim Podfile

配置Podfile内容

flutter_application_path = '/xxxxxxx/my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

platform :ios, '9.1'
target 'myFlutterModule' do
pod 'Masonry'
install_all_flutter_pods(flutter_application_path)
end

flutter_application_path 代表你flutter模块工程的目录路径 找到flutter模块工程之后,找到.ios目录,并在.ios目录中找到Flutter目录下的安装脚本podhelper.rb

flutter模块安装脚本流程如下图所示

image.png

通过查看podhelper脚本代码我们可以看到它主要打包了.ios中的三个目录

def install_flutter_engine_pod
  current_directory = File.expand_path('..', __FILE__)
  engine_dir = File.expand_path('engine', current_directory)
  framework_name = 'Flutter.xcframework'
  copied_engine = File.expand_path(framework_name, engine_dir)
  if !File.exist?(copied_engine)
    # Copy the debug engine to have something to link against if the xcode backend script has not run yet.
    # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
    release_framework_dir = File.join(flutter_root, 'bin', 'cache', 'artifacts', 'engine', 'ios-release')
    unless Dir.exist?(release_framework_dir)
      # iOS artifacts have not been downloaded.
      raise "#{release_framework_dir} must exist. Make sure \"flutter build ios\" has been run at least once"
    end
    FileUtils.cp_r(File.join(release_framework_dir, framework_name), engine_dir)
  end

  # Keep pod path relative so it can be checked into Podfile.lock.
  # Process will be run from project directory.
  engine_pathname = Pathname.new engine_dir
  # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
  project_directory_pathname = defined_in_file.dirname
  relative = engine_pathname.relative_path_from project_directory_pathname

  pod 'Flutter', :path => relative.to_s, :inhibit_warnings => true
end

此方法用于打包上面我们提到的engine目录中的Flutter.xcframework,这个就是我们flutter运行的引擎

def install_flutter_plugin_pods(flutter_application_path)
  flutter_application_path ||= File.join('..', '..')

  # Keep pod path relative so it can be checked into Podfile.lock.
  # Process will be run from project directory.
  ios_project_directory_pathname = Pathname.new File.expand_path(File.join('..', '..'), __FILE__)
  # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
  project_directory_pathname = defined_in_file.dirname
  relative = ios_project_directory_pathname.relative_path_from project_directory_pathname
  pod 'FlutterPluginRegistrant', :path => File.join(relative, 'Flutter', 'FlutterPluginRegistrant'), :inhibit_warnings => true

  symlinks_dir = File.join(relative, '.symlinks', 'plugins')
  FileUtils.mkdir_p(symlinks_dir)

  plugins_file = File.expand_path('.flutter-plugins-dependencies', flutter_application_path)
  plugin_pods = flutter_parse_dependencies_file_for_ios_plugin(plugins_file)
  plugin_pods.each do |plugin_hash|
    plugin_name = plugin_hash['name']
    plugin_path = plugin_hash['path']
    if (plugin_name && plugin_path)
      symlink = File.join(symlinks_dir, plugin_name)
      FileUtils.rm_f(symlink)
      File.symlink(plugin_path, symlink)
      pod plugin_name, :path => File.join(symlink, 'ios'), :inhibit_warnings => true
    end
  end
end

此方法用于打包FlutterPluginRegistrant目录,这里面存放了我们在flutter模块中使用到的第三方插件

def install_flutter_application_pod(flutter_application_path)
  current_directory_pathname = Pathname.new File.expand_path('..', __FILE__)
  app_framework_dir = File.expand_path('App.framework', current_directory_pathname.to_path)
  app_framework_dylib = File.join(app_framework_dir, 'App')
  if !File.exist?(app_framework_dylib)
    # Fake an App.framework to have something to link against if the xcode backend script has not run yet.
    # CocoaPods will not embed the framework on pod install (before any build phases can run) if the dylib does not exist.
    # Create a dummy dylib.
    FileUtils.mkdir_p(app_framework_dir)
    `echo "static const int Moo = 88;" | xcrun clang -x c -dynamiclib -o "#{app_framework_dylib}" -`
  end

  # Keep pod and script phase paths relative so they can be checked into source control.
  # Process will be run from project directory.

  # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
  project_directory_pathname = defined_in_file.dirname
  relative = current_directory_pathname.relative_path_from project_directory_pathname
  pod 'my_flutter', :path => relative.to_s, :inhibit_warnings => true

  flutter_export_environment_path = File.join('${SRCROOT}', relative, 'flutter_export_environment.sh');
  script_phase :name => 'Run Flutter Build my_flutter Script',
    :script => "set -e\nset -u\nsource \"#{flutter_export_environment_path}\"\nexport VERBOSE_SCRIPT_LOGGING=1 && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/xcode_backend.sh build",
    :execution_position => :before_compile
end

此方法用于打包flutter模块源代码编译后的产物

然后运行Pod install 将flutter模块添加进我们的项目中。

这里需要注意的是,一个Pod的target对应一个install_all_flutter_pods。本例子中我们只在myFlutterModule这个target中导入了install_all_flutter_pods,如果有其他target需要使用flutter模块,需要在该target下导入install_all_flutter_pods。

Pod intall执行完毕之后,我们看到成功安装了三个模块

image.png

在CocoaPod生成的工程中,我们可以到这三个模块分别为flutter模块源码编译产物,以及flutter运行引擎和flutter模块使用的插件

image.png

在 Xcode 中打开我们的项目,现在可以使用 ⌘B 编译项目了。

注意

当我们在pubspec.yaml修改了flutter模块依赖的插件,我们就需要在flutter模块的目录中使用flutter pub get,来更新会被podhelper.rb 脚本用到的 plugin 列表,然后再次在我们的Xcode工程中使用Pod install

此外对于flutter模块lib源代码文件中的修改,会在每次Xcode构造IOS工程的时候,通过脚本自动更新。因此我们不需要在使用Pod install来重新安装Pod,在flutter模块中直接修改模块样式或功能,然后直接在Xcode中构建项目。

在IOS工程调用flutter页面

AppDelegate设置

这里推荐直接让我们的AppDelegate继承FlutterAppDelegate,或者你也可以选择不继承,如果不继承的话,你就需要在AppDelegate中实现FlutterAppLifeCycleProvider 协议,来确保 Flutter plugins 接收到必要的回调

为了方便起见,这里我选择的是让AppDelegate继承FlutterAppDelegate

AppDelegate.h文件

@import UIKit;
@import Flutter;


@interface AppDelegate : FlutterAppDelegate 
@property (nonatomic,strong) FlutterEngine *flutterEngine;


@end

AppDelegate.m文件

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"coderjun flutter engine"];
     // Runs the default Dart entrypoint with a default Flutter route.
     [self.flutterEngine run];
     // Used to connect plugins (only if you have plugins with iOS platform code).
     [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
    return [super application:application didFinishLaunchingWithOptions:launchOptions];;
}

现在我已经在AppDelegate中创建了我们自己的flutter引擎。

接下来我们就可以通过这个flutter引擎在ios环境中来运行我们的flutter模块了

使用FlutterEngin展示FlutterViewController

这里我们简单的创建一个ViewController,然后让这个ViewController切换到FlutterViewController

- (void)viewDidLoad {

    UIButton* btn = [UIButton buttonWithType:UIButtonTypeContactAdd];
    btn.center = self.view.center;
    [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
    
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

- (void) btnClick:(UIButton*) sender{
    NSLog(@"tag is %ld",(long)index);
    FlutterEngine *flutterEngine =
         ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
     FlutterViewController *flutterViewController =
         [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
     [self presentViewController:flutterViewController animated:YES completion:nil];
}

当我们点击按钮,ViewController通过模态视图的方式,将我们的FlutterViewControler推到了当前窗口之上。效果如下

m3pe3-0oxj5.gif

结尾

如果文章有什么地方写错了或者有问题,欢迎在评论区留言,我会及时回复。

我的GitHub主页GitHub

会不定期做一些开源项目供大家交流学习