本文最后更新于 2021年4月4日 晚上
通过读书外带写代码, 了解 iOS 平台上和触摸相关的编程内容, 许多内容都来自 Pro iPhone Development with Swift 4
一书.
本系列共 n 篇文章, 旨在提高 iOS 平台上的编程技巧, 具有一定的平台相关性. 由于手势处理是所有移动端开发的重点内容, 故放到首轮讲解.
并且由于 iOS 平台上由于源码封闭, 很难找到这些原理性讲解, 且许多都只是表面现象, 难达本质, 所以不求精, 只求准.
简介
在 iOS 平台上可以针对非常多的手势进行处理, 比如短按, 长按, 侧滑, 3D touch 等等. 下面就来看有关触摸编程的内容, 并实现一些例子.
术语解析
手势(gesture): 手势指的是用户从触摸屏幕到结束触摸所生成的事件, 有若干类型. 手势通过一系列事件(事件流)的形式传递给系统.
手势识别器(gesture recognizer): 是一类对象, 它们可以接收并处理用户手势事件流.
响应链(Responder Chain)
用户手势通过事件流传递进入系统, 而事件流通过响应链(或响应者链)进行实际传递, 故需要了解响应链的内容, 从而理解如何进行手势处理(另外在 iOS 平台上的响应链和 macOS 平台上是类似的).
在一个运行的程序中, 响应链由一系列能够响应用户事件的响应者对象构成, 响应者对象是 UIResponder
的子类对象. UIView
, UIViewController
, UIApplication
, 甚至是 UIApplication
的代理对象(通常是名为 AppDelegate
类的对象)均继承自 UIResponder
.
程序启动后, 首先在 main 函数中创建了 UIApplication
对象和它的代理对象, 而后程序代理对象持有 window, window 持有根视图控制器, 这一条线上的所有对象都是 UIResponder
, 另外所有的视图和视图控制器都是 UIResponder
, 可以看到, 实际上响应者充斥着整个程序.
事件响应过程
顾名思义, 响应链就是响应者组成的一个链式结构.
比如在正常情况下, 第一个响应者就是当前用户进行交互的对象, 这个响应者也成为响应链的 head, 后续响应者和它一起就构成了一个链条(比如它的父视图–>父视图所处的视图控制器, 如此往上推, 最后到达 UIApplication
对象).
事件的处理: 指的是在某个响应者上重写了 UIResponder
的触摸处理方法.
当某个响应者不处理某个事件时(不重写 UIResponder
的触摸处理方法), 该事件就会沿响应链向上传递, 而如果某个响应者能够处理该事件, 则它就将该事件”消费”掉, 从而终止事件的传递. 当然还有一种情况就是响应者接收到事件并进行处理, 而后仍然将事件进行传递.
1 2 3 4 5 6 7
| override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("视图控制器开始触摸") _msgLabel.text = "开始触摸" updateLabelsFrom(touch: touches.first, allTouches: event?.allTouches) next?.touchesBegan(touches, with: event) }
|
通常开发者面对的都是前一种情况.
如果事件经过整个响应链都未被处理, 则该事件会被丢弃.
上面的过程中, 事件又是如何到达第一个响应者的呢?
事件首先被传递给 UIApplication
, 而后传递给程序的 UIWindow
对象, 然后由 UIWindow
对象决定事件的初始响应者(initial responder):
对于触摸事件(手势): UIWindow
对象可以检测用户手势发生在哪个视图上, 而后让该视图作为初始响应者, 开始事件传递过程. 如果该视图或其父视图上有手势识别器, 当触发手势识别器的处理方法后, 事件便不再向后传播. 否则事件会沿响应链传播, 直到遇到处理方法或可以处理的手势识别器.
对于传感器事件(比如摇一摇)或远程控制设备事件, 则直接交给指定的第一响应者(first responder).
如果初始响应者没有处理这个事件, 则沿响应链向上传递, 而当整个链都没有处理该事件, 事件就又回到了 UIWindow
上, 而后被传递给 UIApplication, UIApplication 对象会直接将这个事件交给它的代理对象. 如果 app 代理对象也没有处理这个事件(代理对象不是继承自 UIResponder
, 而是只实现了 UIApplicationDelegate
协议的话), 则事件就被丢弃掉了.
总结起来事件的传递过程是这样的: 事件–>UIApplication(不处理)–>UIWindow(不处理)–>中间处理过程(可能处理)–>沿控制器链条到达 UIWindow(可能处理)–>UIApplication(可能处理, 只要是自定义的话)–> app 代理对象(可能处理)–>事件被丢弃
理解这套模式, 有助于解决许多编程中遇到的问题, 因为有些视图自己就默认带有手势识别器, 如果不知道这个模式, 很难去实现某些复杂效果.
事件传递
在处理用户手势过程中, 如果某个手势识别器拦截了事件, 但没有进行实际处理的话, 实际后续响应者就无法收到手势, 故在某些时候需要手动将事件传递到下一个响应者:
1 2 3 4 5
| if 能够处理事件 { 进行事件处理 } else { 将事件传递给下一个响应者 }
|
并且在处理事件的时候应该遵循:
UIResponder 中 Touch 事件处理的方法
当用户触摸开始后, 系统就会按照之前讲的方式将事件传递给第一个响应者(如果没有手势识别器), 此时该响应者的方法 touchesBegan
会被调用, 比如一个视图控制器上:
1 2 3 4 5 6 7 8 9 10
| override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { let numTaps = touch.tapCount let numTouches = event?.allTouches?.count let myTouches = event?.touches(for: self.view) } }
|
当任意手指离开屏幕, 都会触发 touchesEnded
方法调用.
此外, 如果在触摸过程中有外部事件打断触摸过程(比如来电话), 会触发 touchesCancelled
方法调用, 而之后不会出现 touchesEnded
调用. 故有时为了触摸的一致性, 需要在 touchesCancelled
实现一些操作过程中中断的处理.
示例程序
下面来创建一个可以显示当前触摸有多少个手指, 多少次触摸, 以及 3D Touch 按压力度的程序.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
|
import UIKit import SnapKit
class GestureDemoViewController: UIViewController { private let _msgLabel = UILabel() private let _tapsLabel = UILabel() private let _touchesLabel = UILabel() private let _forceLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() layoutStack() resetLabelText()
let gest = UITapGestureRecognizer(target: self, action: #selector(tapGesture)) view.addGestureRecognizer(gest) }
@objc private func tapGesture() { print("手势识别器的点击事件") }
private func resetLabelText() { _msgLabel.text = "无数据" _tapsLabel.text = "无连续触摸" _touchesLabel.text = "无手指触摸" _forceLabel.text = "无按压力度" _forceLabel.numberOfLines = 0 }
private func layoutStack() { let stack = UIStackView() stack.axis = .vertical stack.distribution = .fillProportionally stack.alignment = .leading view.addSubview(stack)
stack.snp.makeConstraints { $0.center.equalToSuperview() $0.width.equalToSuperview().multipliedBy(0.75) }
stack.addArrangedSubview(getRowWith(title: "信息: ", contentView: _msgLabel)) stack.addArrangedSubview(getRowWith(title: "触摸次数: ", contentView: _tapsLabel)) stack.addArrangedSubview(getRowWith(title: "手指数量: ", contentView: _touchesLabel)) stack.addArrangedSubview(getRowWith(title: "按压力度: ", contentView: _forceLabel)) }
private func getRowWith(title: String, contentView: UIView) -> UIView { let stack = UIStackView() stack.axis = .horizontal let label = UILabel() label.text = title stack.addArrangedSubview(label) stack.addArrangedSubview(contentView) return stack }
private func updateLabelsFrom(touch: UITouch?, allTouches: Set<UITouch>?) { let numTaps = touch?.tapCount ?? 0 _tapsLabel.text = "连续点击次数: \(numTaps)" let numTouches = allTouches?.count ?? 0 _touchesLabel.text = "触摸的手指数量: \(numTouches)" if traitCollection.forceTouchCapability == .available { let force = touch?.force ?? 0 let maxForce = touch?.maximumPossibleForce ?? 0 let forceString = String(format: "%.3f", force) let maxForceString = String(format: "%.3f", maxForce) _forceLabel.text = "按压力度: \(forceString), 支持最大力度: \(maxForceString)" } else { _forceLabel.text = "不支持 3D Touch" } }
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("视图控制器开始触摸") _msgLabel.text = "开始触摸" updateLabelsFrom(touch: touches.first, allTouches: event?.allTouches) }
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { _msgLabel.text = "触摸取消" updateLabelsFrom(touch: touches.first, allTouches: event?.allTouches) }
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { _msgLabel.text = "触摸结束" updateLabelsFrom(touch: touches.first, allTouches: event?.allTouches) }
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { _msgLabel.text = "手指移动" updateLabelsFrom(touch: touches.first, allTouches: event?.allTouches) }
}
|
待续.
响应链中插入有手势识别器的话, 这些手势识别器就相当于是事件拦截器的作用, 且手势识别器不会拦截 began
事件, 因为那个时候还没有手势能识别出来…
更多内容
下一篇我们来看 iOS 平台上的屏幕旋转处理.