GitHub iOS 上架 AppStore

GitHub 最近在 AppStore 上架了 官方 iOS App。即使不使用 PC,也可以处理些 GitHub 上的操作,比如组织任务、issues 反馈、评论、review 以及 merge pull requests 等。

GitHub 通知展示在收件箱中,看起来有点像邮箱的收件箱,你可以收藏或标记它们为已完成。

你也可以使用 emoji 进行评论互动,提升活跃度。

继续阅读“GitHub iOS 上架 AppStore”

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 什么都不需要提供的话,就当作无视。