Flutter与iOS混合开发交互

881 阅读10分钟

1、安装Flutter环境

1、下载SDK并安装

docs.flutter.cn/get-started…

2、 配置环境

如果 ~/.zshenv 文件存在,请在文本编辑器中打开 Zsh 环境变量文件 ~/.zshenv。如果不存在,请创建 ~/.zshenv
export PATH=$HOME/development/flutter/bin:$PATH加入到文件的最后面

创建Flutter项目

以Flutter为主

以Flutter为主:意思是直接创建完整的flutter项目,里面就已经包含了iOS、Android等工程。直接用即可

在需要的目录中 执行 flutter create aiflutter

根据提示执行命令

 In order to run your application, type:
   $ cd flutterdemo
   $ flutter run       // 执行这2行命令
 Your application code is in test/lib/main.dart.

项目配置

进入iOS文件夹

这里需要注意: 需要用到CocosPods将Flutter作为组件导入到项目,但是Flutter并没有直接生成Podfile文件。需要自己init一个

1、进入flutterdemo/ios目录
2、执行 pod init命令
3、执行pod install命令
4、修改Podfile文件,因为项目有flutter的路径信息,文件的配置如下:

source 'https://github.com/CocoaPods/Specs.git'

# Uncomment this line to define a global platform for your project

platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end

在进行 Podfile install时,可能会有警告。 如果想要去掉警告,需要按照以下方式修改。但是修改之后会运行不起来

image.png

正确的应该是选中Debug.xcconfig、Release.xcconfig

image.png

在使用过程中, 因为iOS工程是其他人创建后给我的,在进行pod install的时候,出现了路径找不到的报错: 修改这个路径

image.png

错误处理

  • 错误1:Command PhaseScriptExecution failed with a nonzero exit code

    这是由于Run Script的脚本找不到正确路径 在此确认 podfile文件已修改,具体参照上面 项目配置中的文件

  • 错误2:

    Unable to load contents of file list: '/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Debug-input-files.xcfilelist'

    需要回到Flutter项目目录下,执行flutter run.
    其实在执行flutter create flutterdemo完成的时候,就已经提示了

    In order to run your application, type:
      $ cd test
      $ flutter run
    Your application code is in test/lib/main.dart.
    
  • 错误3

    The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.

    Build Settings中搜索ENABLE_USER_SCRIPT_SANDBOXING 将其设置为NO,如果原本是NO,设置为YES后运行一次后再改为NO
    完全退出Xcode。

  • 错误4

    [!] Invalid Podfile file: cannot load such file -- /Users/huaheshang/packages/flutter_tools/bin/podhelper.

  执行pod install时,提示这个错误

解决方案:
查看ios/Flutter路径下的Generated.xcconfig文件中的路径是否正常

FLUTTER_ROOT路径是否是安装FlutterSDK的路径。eg:/Users/xx/development/flutter FLUTTER_APPLICATION_PATH 项目路径.eg:/Users/xx/flutterdemo

  • 错误5

    Removing xxxx

执行pod install时,将Flutter依赖的一些内容移除掉了

  常见于: 从其他已经配置好的iOS工程中,直接将iOS文件拷贝到另一个Flutter工程中。第一次pod install时。

   解决方案:确保podfile文件的配置正确(参照上面的podfile文件配置)。进入Flutter目录下,执行:flutter run 重新pod install

错误总结

错误总结
1、确保执行过flutter run
2、在Build Settings中搜索ENABLE_USER_SCRIPT_SANDBOXING 将其设置为NO
3、使用pod init新建一个podfile文件并修改里面的内容
4、确认ios/Flutter路径下的Generated.xcconfig中的配置FLUTTER_ROOT、FLUTTER_APPLICATION_PATH是否正常

步骤总结

1、安装FlutterSDK并配置其环境

2、使用命令创建Flutter项目flutter create flutterdemo

3、执行cd flutterdemo 和 flutter run命令

4、导入podfile文件并执行pod install命令

以iOS为主

以iOS为主意思是:手动创建一个iOS工程,将Flutter作为一个组件导入到iOS项目中

1、创建一个iOS工程AIIOSDemo,并进行pod
2、在同级目录下新建Flutter项目:flutter create -t module my_flutter\

image.png 3、在podfile中引入flutter

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '15.0'


# 1、在文件顶部添加 flutter_application_path
flutter_application_path = '../my_flutter'     #这里是刚才创建的flutter module名称
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')


target 'iOSDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  pod 'SnapKit'
  
  pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug']
    
  
  // 2、引入路径
  install_all_flutter_pods(flutter_application_path)
  
end


# 3、添加这个 post_install 块
post_install do |installer|
  flutter_post_install(installer)
end

页面跳转

FlutterEngine

从原生跳转到Flutter页面官方连接

也可参考链接

在使用Flutter之前,需要先注册GeneratedPluginRegistrant

//在AppDelegate中定义全局的flutterEngine
 lazy var flutterEngine: FlutterEngine = FlutterEngine(name: "com.brainco.gameEngine")


 private func initEngine() {
     // 在用到Flutter之前,要先注册这个方法
     //这个要在跳转方法之前运行环境,也可以在appdelegate里面启动就初始化,环境运行需要时间,单写在跳转方法里面靠前位置是不可以的。
     flutterEngine.run();
     GeneratedPluginRegistrant.register(with: flutterEngine);
 }
 
  • 直接以FlutterViewController为页面 在原生页面初始化按钮,并添加点击事件,在事件中实现以下代码:
   
    func jumpToFlutterPage() -> Void {
        
        let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine
        let flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)
        
        /*
        // 可以通过MethodChannel传递参数
        let channel = FlutterMethodChannel(
            name: "com.example.app/flutter",
            binaryMessenger: flutterViewController.binaryMessenger
        )
        
        // 可选 -- 设置初始路由或传递参数
        channel.invokeMethod("initialRoute", arguments: "/targetPage")
         
        // 可选 -- 设置监听,执行Flutter调用原生的方法
        channel.setMethodCallHandler{[weak self] (call, result) in
            guard let strongSelf = self else { return }
            print("flutter 给到我 method:\(call.method) arguments:\(String(describing: call.arguments))")
        }
         */
        self.navigationController?.pushViewController(flutterViewController, animated: true)
    }
  • 将Flutter作为ChildViewController加入原生的viewController

class FlutterCustomViewController: BaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        
        let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine
        let flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)
        
        /*
        // 可以通过MethodChannel传递参数
        let channel = FlutterMethodChannel(
            name: "com.example.app/flutter",
            binaryMessenger: flutterViewController.binaryMessenger
        )
        
        // 可选 -- 设置初始路由或传递参数
        channel.invokeMethod("initialRoute", arguments: "/targetPage")
         
        // 可选 -- 设置监听,执行Flutter调用原生的方法
        channel.setMethodCallHandler{[weak self] (call, result) in
            guard let strongSelf = self else { return }
            print("flutter 给到我 method:\(call.method) arguments:\(String(describing: call.arguments))")
        }
         */
        
        self.addChild(flutterViewController)
        self.view.addSubview(flutterViewController.view)
        flutterViewController.view.snp.makeConstraints{make in
            make.left.right.top.bottom.equalToSuperview()
        }
    }
FlutterEngineGroup

官方demo--github iOSDemo

在项目中,如果存在多个场景需要跳转Flutter。可参考官方提供的demo,使用FlutterEngineGroup的方式。

// 1、 定义一个全局的engines
class FlutterBridgeHelper: NSObject {
 static let engines = FlutterEngineGroup(name: "customEngines", project: nil)
   
   // 2、 创建一个新的engine 并regist插件

    // name: Flutter用来判断展示那个页面的参数
    // initialRoute: 展示页面的初始参数
    public class func createEngine(name: String, initialRoute: String? = nil) -> FlutterEngine {
        let newEngine = FlutterBridgeHelper.engines.makeEngine(withEntrypoint: name, libraryURI: nil, initialRoute: initialRoute)
        GeneratedPluginRegistrant.register(with: newEngine)
        return newEngine
    }

}



// 3、实际调用

func pushToFlutter() {
 // 创建engine 并指定展示的view: “main”
    let newEngine = FlutterBridgeHelper.createEngine(name: "main")
    
 // 根据创建的engine创建一个FlutterViewController
    let flutterController = FlutterViewController(engine: newEngine, nibName: nil, bundle: nil)
    flutterController.view.backgroundColor = .white
    
 // 根据需要定义Flutter与原生之间的交互 MethodChannel、EventChannel等
 //   flutterHelper.flutterRegist(controller: flutterController)
    
    self.navigationController?.pushViewController(flutterController, animated: true)
}

GeneratedPluginRegistrant.register(with: newEngine)这个步骤中,确保register中注册了

// 导入相关头文件
#if __has_include(<connectivity_plus/ConnectivityPlusPlugin.h>)
#import <connectivity_plus/ConnectivityPlusPlugin.h>
#else
@import connectivity_plus;
#endif



+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
    // ....其他插件导入...
    [ConnectivityPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"ConnectivityPlusPlugin"]];
}

iOS与Flutter交互

Flutter 与原生存在三种交互方式 可参考链接

三种 Channel 之间互相独立,各有用途,但它们在设计上却非常相近。每种 Channel 均有三个重要成员变量:

  • name: 【重要参数】String类型,代表 Channel 的名字,也是其唯一标识符需要和Fluter中的定义保持一致
  • messager:【重要参数】BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具
  • codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器

MethodChannel

一般用于传递方法调用(method invocation)通常用于Flutter调用原生中某个方法

举例:使用场景-Flutter需要获取原生生成的用户UUID,并传递UUID做存储操作


// 引入Flutter
import Flutter

@objc class AppDelegate: FlutterAppDelegate {

// 枚举的方式定义方法名
enum FlutterMethodType: String {
    case saveUUID       = "saveUUID"        ///< 保存 UUID
    case getUUID        = "getUUID"         ///< 获取 UUID
}


    let controller = window?.rootViewController as! FlutterViewController

// 初始化参数,并设置回调handle

     func MethodChannelRegist(controller: FlutterViewController) {
        let methodChannel_channer = FlutterMethodChannel(
            name: "com.example/ai/snowflake",
            binaryMessenger: controller.binaryMessenger
        )
        methodChannel_channer.setMethodCallHandler { [weak self] (call, result) in
            guard let self = self else { return }
            self.flutterMethodChanner_channer(call: call, result: result)
        }
    }
    
    // flutter 调用 swift
    private func flutterMethodChanner_channer(call: FlutterMethodCall, result: FlutterResult) -> Void {
        if call.method == FlutterMethodType.getUUID.rawValue {
            let uuid = "uuid"
            result(uuid)
        }else if call.method == FlutterMethodType.saveUUID.rawValue {
            let success = true
            result(success)
        } else {
            result(FlutterMethodNotImplemented)
        }
    }
    
}

image.png

BasicMessageChannel

它是可以双端通信的,Flutter 端可以给 iOS 发送消息,iOS 也可以给 Flutter 发送消息。

// 全局,方便随时可以发送消息
 var basicMessageChannel: FlutterBasicMessageChannel? = nil
 
 // 其他和MethodChannel基本一致
 func BasicMessageChannelRegist(controller: FlutterViewController) {
        basicMessageChannel = FlutterBasicMessageChannel(name: "com.example/ai/snowflake",
                                                             binaryMessenger: controller.binaryMessenger)
        
        basicMessageChannel?.setMessageHandler { [weak self] (call, result) in
            guard let self = self else { return }
            self.flutterMethodChanner_channer(call: call as! FlutterMethodCall, result: result)
        }
        
        // 相比MethodChannel 最重要的区别就是这个 可以主动向Flutter发送消息
        basicMessageChannel?.sendMessage(["name":"隔壁老王","age":25])
        
    }
    
    // flutter 调用 swift
    private func flutterMethodChanner_channer(call: FlutterMethodCall, result: FlutterResult) -> Void {
        if call.method == "methodOne" {
            
        }else if call.method == "methodTwo" {
            
        } else {
            result(FlutterMethodNotImplemented)
        }
    }
 

image.png

EventChannel

只能是原生发送消息给 Flutter 端,例如监听手机电量变化,网络变化,传感器等。

func eventChannelRegist(controller: FlutterViewController) {
        let eventChannel = FlutterEventChannel(
          name: "com.example.demo/event",
          binaryMessenger: controller.binaryMessenger
        )
        eventChannel.setStreamHandler(self)
    }
    
    // MARK: FlutterStreamHandler
    var eventSink: FlutterEventSink? = nil
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.eventSink = events
        return nil
    }
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        self.eventSink = nil
        return nil
    }
    
    func sendEvent(data: Any) {
        eventSink?(data) // 主动发送数据到 Flutter
      }

image.png

交互方式对比

交互方式MethodChannelBasicMessageChannelEventChannel
交互场景Flutter调用原生中某个方法双端通信,可以相互发消息原生发送消息给 Flutter 端
示例flutter获取原生的用户ID/原生监测网络、电量等的变化

交互方法封装与调用

根据上面的单个示例和使用过程中防止单个方法的重复定义,可以将相应的方法进行封装

//
//  FlutterBridgeHelper.swift
//  Runner
//

import UIKit
import Flutter


// MethodChannel的方法
enum FlutterChannelMethodType: String {
    case chan_UUID          = "UUID"
}

// Flutter跳转原生的交互定义
enum FlutterChannelPageType: String {
    case chan_gotoNativePage    = "gotoNativePage"
}



class FlutterBridgeHelper: NSObject {
    // 原生跳转Flutter的engine Group全局定义
    static let engines = FlutterEngineGroup(name: "customEngines", project: nil)
    
    // 与Flutter之间定义的协议 2端必须保持一致
    let channerPrefix = "com.example/ai/"
    let channer_page = "xd_app/"
    
    // 交互的FlutterViewController,原生与Flutter之间交互时,需要拿到FlutterViewController
    var flutterController: FlutterViewController?
    
    // EventChannel的对象 用于原生主动向Flutter传递信息
    var eventSink: FlutterEventSink?
    
    
    override init() {
        super.init()
    }
    
    
    
    // 注册
    func flutterRegist(controller: FlutterViewController) {
        
        self.flutterController = controller
        
        // 注册MethodChanner
        MethodChannelRegist(controller: controller)
        
        // 注册 EventChannel
        EventChannelRegist(controller: controller)
    }
    
    
    
    // 原生调用Flutter
    public func swiftToFlutter(data: Any) {
        eventSink?(data) // 主动发送数据到 Flutter
    }
    
    
    private func MethodChannelRegist(controller: FlutterViewController) {
        
        //注册 MethodChannel   如果有多个MethonChannel的定义,就需要setMethodCallHandler多个
        // 这里将方法之间的交互、页面之间的跳转拆分开了。也可以定义在同一套name协议里
        
        // 方法之间的交互
        let methodChannel = FlutterMethodChannel(
            name: channerPrefix+"snowflake", // "com.example.app/channel",
            binaryMessenger: controller.binaryMessenger
        )
        methodChannel.setMethodCallHandler {[weak self] call, result in
            self?.flutterToSwift(call: call, result: result)
        }
        
        // 页面之间的跳转
        let pageChannel = FlutterMethodChannel(
            name: channer_page+"main_module_channel", // "xd_app/main_module_channel",
            binaryMessenger: controller.binaryMessenger
        )
        pageChannel.setMethodCallHandler {[weak self] call, result in
            self?.flutterToNavPage(call: call, result: result)
        }
    }
    
    
    private func EventChannelRegist(controller: FlutterViewController) {
        // 从 Swift 主动发送事件到 Flutter:
        let eventChannel = FlutterEventChannel(
          name: channerPrefix+"events",  //"com.example.app/events",
          binaryMessenger: controller.binaryMessenger
        )
        eventChannel.setStreamHandler(self)
    }
    
    // flutter 调用 swift
    private func flutterToSwift(call: FlutterMethodCall, result: FlutterResult? = nil) -> Void {
        if call.method == FlutterChannelMethodType.UUID.rawValue {
            // 处理 Flutter 调用
//            let argument = call.arguments as? String ?? ""
            let id = SnowflakeSwift.shared.nextIDString()
            result?(id)
        } else if call.method == xxx {
            // ......
            result?(success)
        } else {
            result?(FlutterMethodNotImplemented)
        }
    }
    
    
    // MARK: page
    private func flutterToNavPage(call: FlutterMethodCall, result: FlutterResult? = nil) -> Void  {
        if call.method == FlutterChannelPageType.chan_gotoNativePage.rawValue {
            // 处理 Flutter 调用
//            let argument = call.arguments as? String ?? ""
            
//            UIViewController.curNavViewController?.pushViewController(IFIMTestViewController(), animated: true)
            UIViewController.curNavViewController?.pushViewController(IFSecondViewController(), animated: true)
            
            result?(nil)
        }
    }
    
    
    
    // MARK: API service
    
    public func api_readUserInfo(completion: @escaping (Error?) -> Void) {
        
        guard let messenger = self.flutterController?.binaryMessenger else {
            completion(NSError(domain: "bridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "信息异常"]))
            return
        }
        
        let userApi = UserApi(binaryMessenger: messenger)
        userApi.getUserInfo { result in
            switch result {
            case .success(let userInfo):
                completion(nil)
                print("获取用户信息成功:(userInfo)")
                // 这里处理 userInfo
            case .failure(let error):
                print("获取用户信息失败:(error)")
                completion(NSError(domain: "bridge", code: -1, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]))
                // 这里处理 error
            }
        }
    }
    
    
    // MARK: helper
    public class func createEngine(name: String, initialRoute: String? = nil) -> FlutterEngine {
        let newEngine = FlutterBridgeHelper.engines.makeEngine(withEntrypoint: name, libraryURI: nil, initialRoute: initialRoute)
        GeneratedPluginRegistrant.register(with: newEngine)
        return newEngine
    }
}


extension FlutterBridgeHelper : FlutterStreamHandler {
    //    MARK: FlutterStreamHandler
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.eventSink = events
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        self.eventSink = nil
        return nil
    }
}

调用

  • 原生跳转Flutter的调用

    •   func SwiftToFlutter() {
            let newEngine = FlutterBridgeHelper.createEngine(name: "main")
            let flutterController = FlutterViewController(engine: newEngine, nibName: nil, bundle: nil)
            // 设置初始路由(可选)
            // flutterViewController.setInitialRoute("/router")
            
            // 按需注册MethodChannel等
            flutterHelper.flutterRegist(controller: flutterController)
            
            flutterController.view.backgroundColor = .white
            self.navigationController?.pushViewController(flutterController, animated: true)
        }
      
  • 原生主动从Flutter获取数据

    •   在上面的FlutterBridgeHelper类中,有一个api_readUserInfo的方法,这个方法的用途是:原生主动从Flutter获取用户的信息,

    •     在这个方法中,UserApi这个类是Flutter通过命令产生的,而不是原生自己定义的。Flutter写好Flutter代码后 通过命令生成Swift/Object-c文件,使用 pigeon 实现 Flutter 与原生通信

  • 每次跳转FlutterViewController的时候,一般来说都需要注册一次交互方法

    • 在APPdelegate启动时,最初就是FlutterViewController,需要注册一次

      • 
        @objc class AppDelegate: FlutterAppDelegate {
        
            let flutterHelper = FlutterBridgeHelper()
            
        // 应用启动时
            override func application(
                _ application: UIApplication,
                didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
            ) -> Bool {
                GeneratedPluginRegistrant.register(with: self)
                let url = launchOptions?[.url]
        
                // 获取 FlutterViewController
                let controller = window?.rootViewController as! FlutterViewController
                
                window.rootViewController = nil
                let nav = IFBaseNavViewController(rootViewController: controller)
                window?.rootViewController = nav
                window.makeKeyAndVisible()
                
                
                // ‼️ 这里需要regist
                flutterHelper.flutterRegist(controller: controller)
               
                
                return super.application(application, didFinishLaunchingWithOptions: launchOptions)
            }
        }
        
    • 原生跳转FlutterViewController时,需要注册

      • 
        class CustomViewController: UIViewController {
        
            let flutterHelper = FlutterBridgeHelper()
        
             func SwiftToFlutter() {
                let newEngine = FlutterBridgeHelper.createEngine(name: "main")
                let flutterController = FlutterViewController(engine: newEngine, nibName: nil, bundle: nil)
                
                // ‼️ 这里需要按需regist
                flutterHelper.flutterRegist(controller: flutterController)
                
                flutterController.view.backgroundColor = .white
                self.navigationController?.pushViewController(flutterController, animated: true)
            }
        }