要点
UIGestureRecognizer — UIKit 中的抽象基类,将手势识别过程分解为独立的对象。每个子类负责一种手势类型:UITapGestureRecognizer — 用于指定数量手指的点击,UISwipeGestureRecognizer — 用于指定方向的滑动。开发者通过 addGestureRecognizer(:) 方法将识别器添加到 UIView,UIKit 自动跟踪触摸、更新状态并在识别时调用操作。
在 UIGestureRecognizer(iOS 3.2,2010)出现之前,开发者重写 UIResponder 方法 — touchesBegan、touchesMoved、touchesEnded — 并手动分析触摸轨迹。这导致代码重复和处理同时触摸时出错。Apple 将此逻辑封装到 UIGestureRecognizer 中,增加了对多点触控、手势取消以及多个识别器在同一视图上同时工作的支持。
根据 WWDC 会议,Gesture Recognizer 在 iPad 上处理多达 11 个同时触摸,在 iPhone 上处理 5 个。在 IT Sectr,我们将 UIGestureRecognizer 作为所有 UIKit 项目中处理用户输入的标准方式 — 这消除了与手动跟踪 touchesBegan 相关的错误。
每个 UIGestureRecognizer 经历 7 种可能的状态,在 UIGestureRecognizer.State 枚举中定义。这些状态反映了识别的生命周期:从检测触摸到手势完成或取消。理解这些状态对于实现自定义识别器和调试冲突至关重要。
| 状态 | 含义 | 何时发生 |
|---|---|---|
| .possible | 初始状态,手势尚未被识别 | 添加到视图后立即 |
| .began | 手势被识别并开始执行 | 手指第一次移动时(pan/longPress) |
| .changed | 手势参数已更改(坐标、角度) | 每次手指移动时 |
| .ended | 用户抬起手指,手势完成 | 在 touchesEnded 时 |
| .cancelled | 手势被系统中断(来电、方向改变) | 在 touchesCancelled 时 |
| .failed | 手势未根据条件被识别 | 在 touchesCancelled 且未识别时 |
| .recognized | .ended 的同义词;手势被成功识别 | 与 .ended 相同 |
离散手势(tap、swipe)从 .possible 直接变为 .ended 或 .failed。连续手势(pan、pinch、rotation、longPress)经历 .possible → .began → .changed(多次)→ .ended。在 action 方法中检查 gestureRecognizer.state — 这可以区分手势的开始、变化和结束。
UIKit 提供 7 个 UIGestureRecognizer 的内置子类,覆盖大多数交互场景。每个子类都有特定的设置:numberOfTapsRequired 用于点击,direction 用于滑动,minimumPressDuration 用于长按。
对于自定义手势(例如绘制锯齿形),创建 UIGestureRecognizer 的子类,重写 touchesBegan、touchesMoved、touchesEnded 方法并更新状态。Apple 建议尽可能使用内置类 — 它们经过优化并能正确相互交互。
当同一个 UIView 上使用多个 UIGestureRecognizer(例如 tap 和 double-tap)时,会产生识别冲突:双击时,单击首先触发。作为解决方案,Apple 提供了 require(toFail:) 方法,该方法延迟一个手势的识别,直到另一个手势失败。
其机制如下:通过调用 tapRecognizer.require(toFail: doubleTapRecognizer),指定 tapRecogniser 仅在 doubleTapRecogniser 以 .failed 结束后才进入 .recognized 状态。这会在执行单击前增加约 0.3 秒的延迟 — 用户双击,第一次点击被忽略。另一种方法 — UIGestureRecognizerDelegate 代理的 gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) 方法,允许同时识别(例如地图上的 pan + pinch)。
根据 WWDC 2020,UIKit 应用中约 15% 的错误与手势冲突的错误配置有关。在 IT Sectr,我们标准化了方法:为每个屏幕创建带有 require(toFail:) 优先级的手势方案 — 这完全消除了双击触发的错误。
为 UIImageView 添加单击处理程序。点击时,图像更改透明度 — 最简单的示例,展示手势与视图的绑定。
import UIKit
class ImageViewController: UIViewController {
@IBOutlet private var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(
target: self,
action: #selector(handleTap(_:))
)
tap.numberOfTapsRequired = 1
imageView.addGestureRecognizer(tap)
imageView.isUserInteractionEnabled = true
}
@objc private func handleTap(_: UITapGestureRecognizer) {
UIView.animate(withDuration: 0.2) {
self.imageView.alpha = self.imageView.alpha == 1.0 ? 0.5 : 1.0
}
}
}
关键点:UIImageView 上的 isUserInteractionEnabled 默认为 false — 没有此标志,Gesture Recognizer 将不会接收触摸。对于 UIView 和 UIButton,该标志默认启用。action 方法接受 UITapGestureRecognizer 参数,通过该参数可以获取 location(in:) 以确定点击坐标。
实现向左滑动返回上一个屏幕。演示方向配置和手势与控制器根视图的绑定。
import UIKit
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swipeLeft = UISwipeGestureRecognizer(
target: self,
action: #selector(handleSwipe(_:))
)
swipeLeft.direction = .left
view.addGestureRecognizer(swipeLeft)
}
@objc private func handleSwipe(_: UISwipeGestureRecognizer) {
navigationController?.popViewController(animated: true)
}
}
UISwipeGestureRecognizer — 离散手势:识别后立即进入 .recognised,没有中间的 .changed。因此,在操作中无需检查状态 — 手势要么被识别(调用操作),要么不被识别。direction 属性接受四个值之一:.left、.right、.up、.down。要支持多个方向,请为每个方向创建单独的识别器。
展示如何在同一视图上配置单击和双击而不产生冲突。双击识别器具有优先级 — 仅当双击未被识别时单击才会触发。
import UIKit
class TapViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let singleTap = UITapGestureRecognizer(
target: self,
action: #selector(handleSingleTap)
)
singleTap.numberOfTapsRequired = 1
let doubleTap = UITapGestureRecognizer(
target: self,
action: #selector(handleDoubleTap)
)
doubleTap.numberOfTapsRequired = 2
singleTap.require(toFail: doubleTap)
view.addGestureRecognizer(singleTap)
view.addGestureRecognizer(doubleTap)
}
@objc private func handleSingleTap() {
print("Single tap — after 0.3s delay")
}
@objc private func handleDoubleTap() {
print("Double tap — instant")
}
}
没有 require(toFail:) 时,双击会先调用 handleSingleTap,然后调用 handleDoubleTap — 这会破坏用户体验。使用 require(toFail:) 后,单击会等待约 0.3 秒以确保第二次点击不会发生。在 IT Sectr,此模式用于图像编辑器和图库中,双击放大,单击选择元素。
常见问题
可以,UIView 同时支持多个 UIGestureRecognizer。要解决冲突,请使用 require(toFail:) 方法,该方法设置识别顺序。要让手势并行工作(例如地图上的 pan + pinch),请实现返回 true 的 gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) 代理方法。
UIGestureRecognizer — 高级抽象,自动识别触摸模式并管理状态。touchesBegan — UIResponder 的低级方法,需要手动跟踪坐标、时序和触摸取消。Gesture Recognizer 更简单、更可靠,是标准手势的首选;touchesBegan 仅适用于自定义图形。
在原生 SwiftUI 中使用修饰符:onTapGesture、onLongPressGesture、DragGesture、MagnificationGesture、RotationGesture。这些是 UIGestureRecognizer 的声明式对应物,集成到 SwiftUI 层级中。如有必要,可以通过 UIViewRepresentable 包装 UIKit 识别器,但 Apple 建议使用原生 SwiftUI 手势。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。