iOS集成Flutter模块

本文主要介绍原生 iOS 应用如何集成 Flutter 模块。

安装 Flutter

Flutter 支持 macOSWindowsLinux

安装步骤可见官网:https://flutter.dev/docs/get-started/install

(注:本教程使用的是 macOS 系统)

iOS 集成 Flutter 模块

通过 Xcode 创建一个 iOS 原生项目,命名为 Example-Flutter

  1. 通过终端进入 Example-Flutter 当前目录下。
  2. 通过如下命令来创建 Flutter 模块。
flutter create --template module my_flutter

将会生成一个 my_flutter 新目录,结构如下:

my_flutter/
├─.ios/
│ ├─Runner.xcworkspace
│ └─Flutter/podhelper.rb
├─lib/
│ └─main.dart
├─test/
└─pubspec.yaml
  1. 通过 CocoaPods 集成 Flutter 模块。

Example-Flutter 项目目录下创建 Podfile 文件,内容编辑如下:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

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

target 'Example-Flutter' do
    install_all_flutter_pods(flutter_application_path)
end

然后,通过 pod install 命令集成 Flutter 模块到 iOS 工程项目中。

输出类似如下日志:

$ pod install
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Installing my_flutter (0.0.1)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `Example-Flutter.xcworkspace` for this project from now on.
Pod installation complete! There are 3 dependencies from the Podfile and 3 total pods installed.

此时,整个项目的目录结构类似是这样的。

some/path/Example-Flutter
├── Example-Flutter/
├── Example-Flutter.xcodeproj
├── Example-Flutter.xcworkspace
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
├── Podfile
├── Podfile.lock
└── Pods/
  1. 添加 Flutter 相关代码

修改 AppDelegate.swift 文件。

import FlutterPluginRegistrant
lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    flutterEngine.run()
    GeneratedPluginRegistrant.register(with: self.flutterEngine);

    return true
}

然后在 iOS 原生页面上添加相应代码。

导入 Flutter package。

import Flutter

增加一个触发按钮,代码如下:

let button = UIButton(type:UIButton.ButtonType.custom)
button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
button.setTitle("Show Flutter!", for: UIControl.State.normal)
button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
button.backgroundColor = UIColor.blue
self.view.addSubview(button)

响应 Button 点击事件,代码如下:

@objc func showFlutter() {
  let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
  let flutterViewController =
      FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
  present(flutterViewController, animated: true, completion: nil)
}

运行效果

  • iOS 原生页面

  • 点击按钮后,显示 Flutter 页面效果。

示例代码

GitHub: iOS-Examples/Example-Flutter

iOS – UI事件传递与响应者链

UI事件传递

UIView 内部包含一个 hitTest 方法,用于 UI 事件的传递。

当一个触摸事件发生时,事件传递顺序如下:

UIApplication -> UIWindow -> 寻找合适的View

假设颜色对应的 View 关系如下:

黄色:View 1
绿色:View 1_1
灰色:View 1_1_1
红色:View 1_2

View 1 定义如下:

class View1: UIView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        print("View 1 hitTest")

        return super.hitTest(point, with: event)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("View 1 touchesBegan")
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("View 1 touchesMoved")
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("View 1 touchesCancelled")
    }
}

同理,View11View111View12 定义类似于 View1

View 的层级关系如下:

View1 -> View11 -> View111

View1 -> View12

具体布局代码如下:

// view 1
let view1 = View1(frame: CGRect(x: 40, y: 100, width: 300, height: 400))
view1.backgroundColor = UIColor.yellow
self.view.addSubview(view1)

// view 1_1
let view11 = View11(frame: CGRect(x: 20, y: 30, width: 100, height: 120))
view11.backgroundColor = UIColor.green
view1.addSubview(view11)

// view 1_1_1
let view111 = View111(frame: CGRect(x: 10, y: 10, width: 60, height: 40))
view111.backgroundColor = UIColor.gray
view11.addSubview(view111)

// view 1_2
let view12 = View12(frame: CGRect(x: 20, y: 200, width: 100, height: 120))
view12.backgroundColor = UIColor.red
view1.addSubview(view12)

当一个触摸事件发生在灰色(View 1_1_1)区域时,hitTest 被调用的顺序如下。

View 1 hitTest
View 1_2 hitTest
View 1_1 hitTest
View 1_1_1 hitTest

表明触摸事件发生时,先从底层 View 开始逐级向上层进行遍历,直到找到灰色视图 View111

响应者链

寻找到合适的 View 后,将会响应对应 View 的触摸事件。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    print("View 1_1_1 touchesBegan")
}

此处合适的 View 就是 View111,将输出日志:

View 1_1_1 touchesBegan

响应者链的传递顺序与 UI 事件传递顺序正好相反,如果当前 View (即:View 1_1_1)无法响应触摸事件(比如:isUserInteractionEnabled = false),将向父一级传递事件。此时,View 1_1 将收到触摸事件,如果 View 1_1 可交互的话,将响应对应的 touchesBegan 事件。

View 1_1 touchesBegan

否则继续往父一级遍历,直到找到合适的响应对象。

示例代码

GitHub: iOS-Examples/Example-View响应链

iOS – UIView和CALayer的关系

CALayer 主要负责显示内容,继承自 NSObject

UIView 主要对 CALayer 做了简单的封装(UIView 类中有个成员变量 layer 就是 CALayer 类型)。另外,UIView 继承自 UIResponder 类,所以也会负责处理触摸事件的响应。

  • UIView 部分源码如下
open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate {

    open class var layerClass: AnyClass { get }

    public init(frame: CGRect)

    public init?(coder: NSCoder)

    open var isUserInteractionEnabled: Bool

    open var tag: Int

    open var layer: CALayer { get }

    @available(iOS 9.0, *)
    open var canBecomeFocused: Bool { get }

    @available(iOS 9.0, *)
    open var isFocused: Bool { get }

    @available(iOS 9.0, *)
    open var semanticContentAttribute: UISemanticContentAttribute

    ......
}

当对一个视图进行绘制的时候,绘图单元会向 CALayer 索取要显示元素的相关数据,此时, CALayer 会通过 delegate 通知到 UIView,看看 UIView 是否有提供需要绘制的元素。如果 UIView 什么都不需要提供的话,就当作无视。