触摸事件概述
事件类型
┌─────────────────────────────────────────────────────────────────────┐
│ iOS 事件类型总览 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Touch Events │ │ Motion Events │ │ Remote Events │ │
│ │ 触摸事件 │ │ 运动事件 │ │ 远程控制事件 │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ • 手指触摸屏幕 │ │ • 摇一摇 │ │ • 耳机线控 │ │
│ │ • 多点触控 │ │ • 加速度计 │ │ • 蓝牙控制 │ │
│ │ • 3D Touch │ │ • 陀螺仪 │ │ • CarPlay │ │
│ │ • Apple Pencil │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ Press Events │ iOS 9+ 物理按键事件 │
│ │ 按压事件 │ (Apple TV Remote, 游戏手柄等) │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
UITouch 生命周期
swift
/*
═══════════════════════════════════════════════════════════════════
UITouch 状态流转
═══════════════════════════════════════════════════════════════════
手指按下 手指移动 手指抬起
│ │ │
▼ ▼ ▼
┌──────┐ 移动中 ┌──────┐ 移动结束 ┌──────┐
│ Began │ ─────────→│ Moved │ ──────────────→│ Ended │
└──────┘ └──────┘ └──────┘
│ │ │
│ │ │
│ 系统中断(如来电) │ │
│ │ │ │
│ ▼ │ │
│ ┌──────────┐ │ │
└───→│Cancelled │←────┘ │
└──────────┘ │
│ │
└────────────────────────────────────┘
事件结束
*/
// MARK: - UITouch 核心属性
extension UITouch {
/// 触摸阶段
public enum Phase: Int {
case began // 手指触摸屏幕
case moved // 手指在屏幕上移动
case stationary // 手指在屏幕上但没有移动
case ended // 手指离开屏幕
case cancelled // 系统取消触摸(如来电)
@available(iOS 13.4, *)
case regionEntered // 指针进入区域(iPadOS 光标)
@available(iOS 13.4, *)
case regionMoved // 指针在区域内移动
@available(iOS 13.4, *)
case regionExited // 指针离开区域
}
/// 触摸类型
public enum TouchType: Int {
case direct // 直接触摸(手指)
case indirect // 间接触摸(Apple TV Remote)
case pencil // Apple Pencil
@available(iOS 13.4, *)
case indirectPointer // 间接指针(鼠标/触控板)
}
}
// MARK: - UITouch 信息获取示例
class TouchInfoView: UIView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
// 基本信息
let location = touch.location(in: self) // 在当前视图中的位置
let previousLocation = touch.previousLocation(in: self) // 上一次位置
let timestamp = touch.timestamp // 时间戳
let tapCount = touch.tapCount // 点击次数(双击等)
let phase = touch.phase // 当前阶段
let type = touch.type // 触摸类型
// 压力信息(3D Touch / Apple Pencil)
let force = touch.force // 当前压力
let maximumPossibleForce = touch.maximumPossibleForce // 最大压力
let normalizedForce = force / maximumPossibleForce // 归一化压力 0~1
// Apple Pencil 专属
if touch.type == .pencil {
let altitudeAngle = touch.altitudeAngle // 倾斜角度(与屏幕平面)
let azimuthAngle = touch.azimuthAngle(in: self) // 方位角
let azimuthVector = touch.azimuthUnitVector(in: self) // 方位向量
}
// 触摸半径(估计)
let majorRadius = touch.majorRadius // 触摸区域半径
let majorRadiusTolerance = touch.majorRadiusTolerance // 容差
print("""
📍 Touch Info:
Location: \(location)
Phase: \(phase)
TapCount: \(tapCount)
Force: \(normalizedForce)
""")
}
}
UIEvent 事件容器
swift
/*
═══════════════════════════════════════════════════════════════════
UIEvent 结构
═══════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ UIEvent │
├─────────────────────────────────────────────────────────────────┤
│ type: EventType // 事件类型 │
│ subtype: EventSubtype // 子类型 │
│ timestamp: TimeInterval // 时间戳 │
├─────────────────────────────────────────────────────────────────┤
│ allTouches: Set<UITouch>? // 所有触摸点 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Touch 1 │ │ Touch 2 │ │ Touch 3 │ ...多点触控 │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
*/
// UIEvent 类型
extension UIEvent {
public enum EventType: Int {
case touches // 触摸事件
case motion // 运动事件
case remoteControl // 远程控制事件
case presses // 按压事件
@available(iOS 13.4, *)
case scroll // 滚动事件
@available(iOS 13.4, *)
case hover // 悬停事件
case transform // 变换事件
}
}
// 获取特定视图上的触摸
extension UIEvent {
func touches(for view: UIView) -> Set<UITouch>? {
return allTouches?.filter { $0.view == view }
}
func touches(for window: UIWindow) -> Set<UITouch>? {
return allTouches?.filter { $0.window == window }
}
func touches(for gestureRecognizer: UIGestureRecognizer) -> Set<UITouch>? {
return allTouches?.filter { touch in
touch.gestureRecognizers?.contains(gestureRecognizer) ?? false
}
}
}
事件传递全流程图解
完整事件传递链路
═══════════════════════════════════════════════════════════════════════════
触摸事件完整传递流程
═══════════════════════════════════════════════════════════════════════════
┌──────────────────────────────────────────────────────────────────────┐
│ 硬件层 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 触摸屏幕硬件 │ │
│ │ 检测到手指触摸,生成触摸数据 │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
└────────────────────────────────│────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ IOKit 层 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ IOHIDEvent(硬件事件) │ │
│ │ 封装触摸数据为 IOHIDEvent 事件 │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
└────────────────────────────────│────────────────────────────────────┘
│
│ Mach Port 传递
▼
┌──────────────────────────────────────────────────────────────────────┐
│ SpringBoard 进程 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 接收 IOHIDEvent,判断前台 App,通过 Mach Port 转发给 App 进程 │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
└────────────────────────────────│────────────────────────────────────┘
│
│ Mach Port 传递
▼
┌──────────────────────────────────────────────────────────────────────┐
│ App 进程 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Source1(Mach Port) │ │
│ │ RunLoop 的 Source1 接收到 Mach Port 消息 │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Source0(触发) │ │
│ │ Source1 触发 Source0,将事件封装为 UIEvent │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ UIApplication │ │
│ │ application.sendEvent(event) → 发送到 UIWindow │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ UIWindow │ │
│ │ 执行 Hit-Test 寻找最佳响应视图 │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┴─────────────────────┐ │
│ │ Hit-Test 过程 │ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ hitTest:withEvent: │ │ │
│ │ │ pointInside:withEvent: │ │ │
│ │ │ 从后向前遍历子视图 │ │ │
│ │ │ 递归查找最深层可响应视图 │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 找到 Hit-Test View │ │
│ │ (最适合处理触摸的视图) │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 手势识别器 │ ←─ 同时接收事件 ─→ │ 触摸方法 │ │
│ │ Gesture │ │ touches... │ │
│ │ Recognizers │ │ 方法 │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ │ 手势识别成功 │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 响应者链传递 │ │
│ │ HitTestView → SuperView → ... → ViewController → Window │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════
时序图
═══════════════════════════════════════════════════════════════════════════
事件传递时序图
═══════════════════════════════════════════════════════════════════════════
时间 →
│
│ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌────────┐ ┌─────┐ ┌─────────┐
│ │Hardware │ │SpringBoard│ │RunLoop │ │UIApp │ │UIWin│ │HitTestV │
│ └────┬────┘ └─────┬────┘ └───┬────┘ └───┬────┘ └──┬──┘ └────┬────┘
│ │ │ │ │ │ │
│ │ IOHIDEvent │ │ │ │ │
│ │───────────→│ │ │ │ │
│ │ │ │ │ │ │
│ │ │ Mach Msg │ │ │ │
│ │ │─────────→│ │ │ │
│ │ │ │ │ │ │
│ │ │ │ Source1 │ │ │
│ │ │ │─────────→│ │ │
│ │ │ │ │ │ │
│ │ │ │ UIEvent │ │ │
│ │ │ │←─────────│ │ │
│ │ │ │ │ │ │
│ │ │ │ sendEvent │ │
│ │ │ │──────────┼────────→│ │
│ │ │ │ │ │ │
│ │ │ │ │ hitTest │ │
│ │ │ │ │────────→│ │
│ │ │ │ │ │ │
│ │ │ │ │ │ 递归查找 │
│ │ │ │ │ │─────────→│
│ │ │ │ │ │ │
│ │ │ │ │ │ 返回View │
│ │ │ │ │ │←─────────│
│ │ │ │ │ │ │
│ │ │ │ │ HitView │ │
│ │ │ │ │←────────│ │
│ │ │ │ │ │ │
│ │ │ │ 分发 touches... │ │
│ │ │ │ │─────────┼─────────→│
│ │ │ │ │ │ │
▼ │ │ │ │ │ │
═══════════════════════════════════════════════════════════════════════════
Hit-Test 机制详解
Hit-Test 核心算法
swift
// MARK: - Hit-Test 默认实现(伪代码)
/*
═══════════════════════════════════════════════════════════════════
Hit-Test 算法流程
═══════════════════════════════════════════════════════════════════
hitTest:withEvent: 方法流程:
┌─────────────────────────────────────────────────────────────┐
│ 1. 检查自身是否可以接收事件 │
│ • hidden == false │
│ • userInteractionEnabled == true │
│ • alpha > 0.01 │
└─────────────────────────┬───────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ 不满足条件 │ 满足条件
▼ ▼
返回 nil ┌─────────────────────┐
│ 2. 检查点是否在自身范围内 │
│ pointInside:withEvent │
└───────────┬─────────────┘
│
┌───────────┴───────────┐
│ 不在范围内 │ 在范围内
▼ ▼
返回 nil ┌─────────────────┐
│ 3. 倒序遍历子视图 │
│ (后添加的先遍历) │
└────────┬────────┘
│
┌────────▼────────┐
│ 4. 递归调用子视图的 │
│ hitTest 方法 │
└────────┬────────┘
│
┌────────┴────────┐
│ 子视图返回非nil │ 全部返回nil
▼ ▼
返回该子视图 返回自己
*/
extension UIView {
/// Hit-Test 默认实现(系统实现的等效代码)
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 1. 检查是否可以接收事件
guard isUserInteractionEnabled,
!isHidden,
alpha > 0.01 else {
return nil
}
// 2. 检查点是否在自身范围内
guard point(inside: point, with: event) else {
return nil
}
// 3. 倒序遍历子视图(后添加的视图在上层,优先响应)
for subview in subviews.reversed() {
// 坐标转换:将点从当前视图坐标系转换到子视图坐标系
let convertedPoint = subview.convert(point, from: self)
// 4. 递归调用子视图的 hitTest
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
// 5. 没有子视图响应,返回自己
return self
}
/// 判断点是否在视图范围内(默认实现)
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.contains(point)
}
}
Hit-Test 可视化演示
swift
// MARK: - Hit-Test 过程可视化
/*
═══════════════════════════════════════════════════════════════════
Hit-Test 遍历示例
═══════════════════════════════════════════════════════════════════
视图层级:
┌─────────────────────────────────────────────────────────────────┐
│ Window │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ RootView │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ ViewA │ │ ViewB │ │ │
│ │ │ (subviews[0]) │ │ (subviews[1]) │ │ │
│ │ │ │ │ ┌───────────┐ ┌─────────┐ │ │ │
│ │ │ │ │ │ ViewB1 │ │ ViewB2 │ │ │ │
│ │ │ │ │ │ [0] │ │ [1] │ │ │ │
│ │ │ │ │ │ │ │ ✕ │ │ │ │
│ │ │ │ │ │ │ │ 触摸点 │ │ │ │
│ │ │ │ │ └───────────┘ └─────────┘ │ │ │
│ │ └─────────────────────┘ └─────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Hit-Test 遍历顺序(假设触摸点在 ViewB2 上):
① Window.hitTest
│
├─ 检查 Window: ✓ 可交互 ✓ 点在范围内
│
└─② RootView.hitTest(Window 唯一子视图)
│
├─ 检查 RootView: ✓ 可交互 ✓ 点在范围内
│
├─ 倒序遍历子视图...
│
├─③ ViewB.hitTest (subviews[1],后添加,先遍历)
│ │
│ ├─ 检查 ViewB: ✓ 可交互 ✓ 点在范围内
│ │
│ ├─ 倒序遍历子视图...
│ │
│ ├─④ ViewB2.hitTest (subviews[1])
│ │ │
│ │ ├─ 检查 ViewB2: ✓ 可交互 ✓ 点在范围内
│ │ ├─ 无子视图
│ │ └─ 返回 ViewB2 ✓ ←─── 找到目标!
│ │
│ └─ 返回 ViewB2(子视图找到结果,不再遍历 ViewB1)
│
└─ 返回 ViewB2(子视图找到结果,不再遍历 ViewA)
最终结果:ViewB2 成为 Hit-Test View
*/
自定义 Hit-Test 实战
swift
// MARK: - 扩大按钮点击区域
class ExpandedButton: UIButton {
/// 扩展的点击区域(负值表示向外扩展)
var touchAreaInsets: UIEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// 扩大判定区域
let expandedBounds = bounds.inset(by: touchAreaInsets)
return expandedBounds.contains(point)
}
}
// 使用示例
let button = ExpandedButton()
button.touchAreaInsets = UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -20) // 四周各扩大20pt
// MARK: - 让子视图超出父视图部分也能响应
class OverflowContainerView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 先检查子视图(即使超出范围)
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
// 不使用 pointInside,直接让子视图判断
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
// 子视图都没响应,再判断自身
if point(inside: point, with: event) {
return self
}
return nil
}
}
/*
使用场景:
┌─────────────────────────────┐
│ ContainerView │
│ │
│ ┌─────────────────┐ │
│ │ PopupView ├─┼──┐ ← 弹出视图超出容器
│ │ │ │ │
│ └─────────────────┘ │ │
│ │ │
└─────────────────────────────┘ │
│
触摸这里也能响应 ─┘
*/
// MARK: - 穿透视图(让事件传递到下层)
class PassthroughView: UIView {
/// 需要穿透的子视图类型
var passthroughViews: [UIView.Type] = []
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
// 如果点击到自身或指定类型的视图,返回 nil 让事件穿透
if hitView === self {
return nil
}
// 检查是否是需要穿透的视图类型
if let hitView = hitView {
for viewType in passthroughViews {
if type(of: hitView) == viewType {
return nil
}
}
}
return hitView
}
}
/*
使用场景:遮罩层穿透
┌─────────────────────────────────────┐
│ BottomView(可点击的按钮等) │
│ ┌─────────────────────────────┐ │
│ │ PassthroughView(半透明遮罩) │ │ ← 点击遮罩区域
│ │ ┌───────────────┐ │ │ 穿透到 BottomView
│ │ │ 弹窗内容 │ │ │
│ │ │ (可点击) │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
*/
// MARK: - 自定义点击区域形状(圆形按钮)
class CircleButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// 计算圆心和半径
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let radius = min(bounds.width, bounds.height) / 2
// 计算点到圆心的距离
let dx = point.x - center.x
let dy = point.y - center.y
let distance = sqrt(dx * dx + dy * dy)
// 距离小于半径则在圆内
return distance <= radius
}
}
// MARK: - 多区域响应(一个View内有多个可点击区域)
class MultiTapAreaView: UIView {
struct TapArea {
let rect: CGRect
let handler: () -> Void
}
var tapAreas: [TapArea] = []
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// 查找点击的区域
for area in tapAreas {
if area.rect.contains(location) {
area.handler()
return
}
}
}
}
Hit-Test 特殊情况处理
swift
// MARK: - ScrollView 内按钮延迟响应问题
/*
问题:UIScrollView 默认 delaysContentTouches = true
会延迟 150ms 判断是滑动还是点击,导致按钮响应慢
*/
class FastResponseScrollView: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
// 关闭延迟,让内容立即响应
delaysContentTouches = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
delaysContentTouches = false
}
// 防止 ScrollView 取消按钮的触摸
override func touchesShouldCancel(in view: UIView) -> Bool {
// 如果是 UIControl(按钮等),不取消触摸
if view is UIControl {
return false
}
return super.touchesShouldCancel(in: view)
}
}
// MARK: - TableView Cell 内按钮点击
class CellWithButton: UITableViewCell {
let actionButton = UIButton()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(actionButton)
// 重要:按钮添加到 contentView,而不是 cell 本身
// 这样 TableView 的选择和按钮点击可以独立工作
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 先检查按钮
let buttonPoint = actionButton.convert(point, from: self)
if actionButton.point(inside: buttonPoint, with: event) {
return actionButton
}
// 其他区域走默认逻辑
return super.hitTest(point, with: event)
}
}
// MARK: - 手势冲突解决
class GestureConflictView: UIView {
let tapGesture = UITapGestureRecognizer()
let innerButton = UIButton()
override init(frame: CGRect) {
super.init(frame: frame)
addGestureRecognizer(tapGesture)
addSubview(innerButton)
// 解决方案1:让手势在按钮区域失效
tapGesture.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension GestureConflictView: UIGestureRecognizerDelegate {
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool {
// 如果触摸到按钮,手势不接收
let location = touch.location(in: self)
if innerButton.frame.contains(location) {
return false
}
return true
}
}
响应者链(Responder Chain)
响应者链结构
═══════════════════════════════════════════════════════════════════════════
响应者链结构
═══════════════════════════════════════════════════════════════════════════
UIResponder 继承体系:
┌──────────────┐
│ UIResponder │ ← 抽象基类
└──────┬───────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌───────────────┐ ┌──────────────────┐
│ UIView │ │UIViewController│ │ UIApplication │
└──────┬──────┘ └───────────────┘ └──────────────────┘
│
├─────────────────┬─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UIWindow │ │ UIControl │ │UIScrollView │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│UIButton │ │UISlider │ │UISwitch │
└─────────┘ └─────────┘ └─────────┘
响应者链传递路径示例:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ View │───→│SuperView│───→│ViewController│───→│ Window │ │
│ │(Initial)│ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │UIApplication│───→│ AppDelegate │───→│ nil(事件被丢弃) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════
UIResponder 核心方法
swift
// MARK: - UIResponder 核心方法
open class UIResponder: NSObject {
// ═══════════════════════════════════════════════════════════════
// MARK: - 响应者链
// ═══════════════════════════════════════════════════════════════
/// 下一个响应者
open var next: UIResponder? { get }
/// 是否可以成为第一响应者
open var canBecomeFirstResponder: Bool { get } // 默认 false
/// 成为第一响应者
open func becomeFirstResponder() -> Bool
/// 是否可以放弃第一响应者
open var canResignFirstResponder: Bool { get } // 默认 true
/// 放弃第一响应者
open func resignFirstResponder() -> Bool
/// 是否是第一响应者
open var isFirstResponder: Bool { get }
// ═══════════════════════════════════════════════════════════════
// MARK: - 触摸事件
// ═══════════════════════════════════════════════════════════════
/// 触摸开始
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
/// 触摸移动
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
/// 触摸结束
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
/// 触摸取消
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
/// 触摸预估更新(Apple Pencil 等)
open func touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>)
// ═══════════════════════════════════════════════════════════════
// MARK: - 按压事件(Apple TV Remote 等)
// ═══════════════════════════════════════════════════════════════
open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)
// ═══════════════════════════════════════════════════════════════
// MARK: - 运动事件
// ═══════════════════════════════════════════════════════════════
open func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
// ═══════════════════════════════════════════════════════════════
// MARK: - 远程控制事件
// ═══════════════════════════════════════════════════════════════
open func remoteControlReceived(with event: UIEvent?)
}
响应者链详细分析
swift
// MARK: - 不同场景的响应者链路径
/*
═══════════════════════════════════════════════════════════════════
场景1:普通视图层级
═══════════════════════════════════════════════════════════════════
视图结构:
Window
└── RootViewController.view
└── ContainerView
└── TargetView ← 触摸点
响应者链:
TargetView → ContainerView → RootViewController → Window →
UIApplication → AppDelegate → nil
═══════════════════════════════════════════════════════════════════
场景2:多层 ViewController 嵌套
═══════════════════════════════════════════════════════════════════
结构:
Window
└── NavigationController.view
└── ParentViewController.view
└── ChildViewController.view(通过 addChild 添加)
└── TargetView ← 触摸点
响应者链:
TargetView → ChildVC.view → ChildViewController →
ParentVC.view → ParentViewController →
NavController.view → NavigationController →
Window → UIApplication → AppDelegate → nil
═══════════════════════════════════════════════════════════════════
场景3:Modal 弹出
═══════════════════════════════════════════════════════════════════
结构:
Window
├── RootViewController.view
└── PresentedViewController.view(模态弹出)
└── TargetView ← 触摸点
响应者链:
TargetView → PresentedViewController.view →
PresentedViewController → Window →
UIApplication → AppDelegate → nil
注意:不会经过 RootViewController!
═══════════════════════════════════════════════════════════════════
场景4:UIAlertController
═══════════════════════════════════════════════════════════════════
响应者链:
AlertAction → AlertContentView → AlertController.view →
UIAlertController → _UIAlertControllerWindow(独立的 Window)→
UIApplication → AppDelegate → nil
*/
// MARK: - 查看响应者链
extension UIResponder {
/// 打印完整响应者链
func printResponderChain() {
var responder: UIResponder? = self
var chain: [String] = []
while let current = responder {
let name = String(describing: type(of: current))
chain.append(name)
responder = current.next
}
chain.append("nil")
print("═══════════════════════════════════════")
print("📍 Responder Chain:")
print(chain.joined(separator: " → "))
print("═══════════════════════════════════════")
}
}
// 使用
class SomeView: UIView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
printResponderChain()
super.touchesBegan(touches, with: event)
}
}
事件传递 vs 响应者链
swift
/*
═══════════════════════════════════════════════════════════════════
事件传递 vs 响应者链:方向相反!
═══════════════════════════════════════════════════════════════════
事件传递(Hit-Test)
════════════════════
从上到下,从外到内
UIApplication
│
▼
UIWindow
│
▼
RootView ─────────────────┐
│ │
▼ │
ViewA │
│ │ 寻找最合适的
▼ │ 响应视图
ViewB │
│ │
▼ │
ViewC ← 找到了! │
│
▼
响应者链(事件处理)
════════════════════
从下到上,从内到外
nil ← 最终丢弃
▲
│
AppDelegate
▲
│
UIApplication
▲
│
UIWindow
▲
│
ViewController
▲
│ 如果 ViewC 不处理
RootView ←─────────┐ 事件沿响应者链
▲ │ 向上传递
│ │
ViewA │
▲ │
│ │
ViewB │
▲ │
│ │
ViewC ───────────┘
(First Responder)
═══════════════════════════════════════════════════════════════════
*/
自定义响应者链
swift
// MARK: - 自定义 next Responder
/// 让视图的 next 指向指定的响应者
class CustomNextResponderView: UIView {
weak var customNextResponder: UIResponder?
override var next: UIResponder? {
return customNextResponder ?? super.next
}
}
// MARK: - 事件转发示例
class EventForwardingView: UIView {
/// 事件转发目标
weak var forwardTarget: UIView?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 转发给目标视图
forwardTarget?.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
forwardTarget?.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
forwardTarget?.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
forwardTarget?.touchesCancelled(touches, with: event)
}
}
// MARK: - 使用 Target-Action 沿响应者链发送消息
/// 响应者链 Target-Action(类似 UIControl 的机制)
extension UIResponder {
/// 沿响应者链查找能响应指定 selector 的对象
func findResponder(for selector: Selector) -> UIResponder? {
var responder: UIResponder? = self
while let current = responder {
if current.responds(to: selector) {
return current
}
responder = current.next
}
return nil
}
/// 沿响应者链发送消息(第一个能响应的对象会处理)
@discardableResult
func sendAction(_ action: Selector, to target: Any? = nil, with sender: Any? = nil) -> Bool {
// 如果指定了 target,直接发送
if let target = target as? NSObject, target.responds(to: action) {
target.perform(action, with: sender)
return true
}
// 沿响应者链查找
if let responder = findResponder(for: action) as? NSObject {
responder.perform(action, with: sender)
return true
}
return false
}
}
// MARK: - 使用示例
class ButtonView: UIView {
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// 沿响应者链发送消息,不关心谁来处理
sendAction(#selector(ResponderActions.handleButtonTap(_:)), with: self)
}
}
// 定义可响应的方法
@objc protocol ResponderActions {
@objc optional func handleButtonTap(_ sender: Any?)
}
// ViewController 处理
class SomeViewController: UIViewController, ResponderActions {
func handleButtonTap(_ sender: Any?) {
print("按钮被点击了!")
}
}
手势识别器(Gesture Recognizer)
手势识别器与触摸事件的关系
═══════════════════════════════════════════════════════════════════════════
手势识别器与触摸事件的竞争关系
═══════════════════════════════════════════════════════════════════════════
事件分发流程:
┌──────────────────────┐
│ UIEvent │
│ (包含 UITouch) │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Hit-Test View 确定 │
└──────────┬───────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 手势识别器 │ 同时 │ 触摸方法 │
│ (Gesture │ 接收 │ (touches...) │
│ Recognizers) │ 事件 │ │
└────────┬────────┘ └────────┬────────┘
│ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ 手势识别中 │ │ touchesBegan │
│ (Possible) │ │ touchesMoved │
└──────┬──────┘ └────────┬────────┘
│ │
┌──────┴──────┐ │
│ │ │
▼ ▼ │
识别成功 识别失败 │
(Recognized) (Failed) │
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────┐
│ │
│ 手势成功 → 默认取消触摸方法(touchesCancelled)│
│ 手势失败 → 触摸方法继续正常执行 │
│ │
└──────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════
手势状态机
swift
/*
═══════════════════════════════════════════════════════════════════
手势识别器状态机
═══════════════════════════════════════════════════════════════════
离散型手势(Tap, Swipe 等):
─────────────────────────────
┌───────────────────┐
│ Possible │ ← 初始状态
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Recognized │ │ Failed │
│ (识别成功) │ │ (识别失败) │
└─────────────┘ └─────────────┘
│ │
└─────────┬─────────┘
▼
┌───────────────────┐
│ Possible │ ← 重置后等待下次
└───────────────────┘
连续型手势(Pan, Pinch, Rotation 等):
─────────────────────────────────────
┌───────────────────┐
│ Possible │ ← 初始状态
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Began │ │ Failed │
│ (开始识别) │ │ (识别失败) │
└──────┬──────┘ └─────────────┘
│
▼
┌─────────────┐
│ Changed │ ←───┐
│ (持续变化) │ │ 循环
└──────┬──────┘ ────┘
│
┌─────┴─────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Ended │ │Cancelled │
│ (正常结束)│ │ (被取消) │
└──────────┘ └──────────┘
│ │
└─────┬─────┘
▼
┌─────────────┐
│ Possible │ ← 重置等待下次
└─────────────┘
*/
extension UIGestureRecognizer {
public enum State: Int {
case possible // 尚未识别(初始状态)
case began // 连续手势开始
case changed // 连续手势变化中
case ended // 连续手势结束 / 离散手势识别成功
case cancelled // 手势被取消
case failed // 手势识别失败
// recognized 是 ended 的别名
public static var recognized: State { return .ended }
}
}
// MARK: - 监控手势状态变化
class GestureStateLogger {
static func logState(_ gesture: UIGestureRecognizer) {
let stateName: String
switch gesture.state {
case .possible: stateName = "Possible"
case .began: stateName = "Began"
case .changed: stateName = "Changed"
case .ended: stateName = "Ended/Recognized"
case .cancelled: stateName = "Cancelled"
case .failed: stateName = "Failed"
@unknown default: stateName = "Unknown"
}
print("🤚 [\(type(of: gesture))] State: \(stateName)")
}
}
手势识别器属性详解
swift
// MARK: - UIGestureRecognizer 核心属性
extension UIGestureRecognizer {
// ═══════════════════════════════════════════════════════════════
// 取消触摸方法相关
// ═══════════════════════════════════════════════════════════════
/// 手势识别成功后,是否取消触摸方法
/// 默认 true:手势成功会调用 touchesCancelled
open var cancelsTouchesInView: Bool { get set }
/// 手势识别期间,是否延迟发送 touchesBegan
/// 默认 false
open var delaysTouchesBegan: Bool { get set }
/// 手势识别失败后,是否延迟发送 touchesEnded
/// 默认 true
open var delaysTouchesEnded: Bool { get set }
// ═══════════════════════════════════════════════════════════════
// 多手势协作相关
// ═══════════════════════════════════════════════════════════════
/// 代理
weak open var delegate: UIGestureRecognizerDelegate? { get set }
/// 设置需要另一个手势失败后才能识别
open func require(toFail otherGestureRecognizer: UIGestureRecognizer)
}
// MARK: - UIGestureRecognizerDelegate
public protocol UIGestureRecognizerDelegate: NSObjectProtocol {
/// 手势是否应该开始识别
optional func gestureRecognizerShouldBegin(
_ gestureRecognizer: UIGestureRecognizer
) -> Bool
/// 是否允许同时识别多个手势
optional func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool
/// 是否需要另一个手势失败后才能识别
optional func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
) -> Bool
/// 另一个手势是否需要等待当前手势失败
optional func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
) -> Bool
/// 手势是否应该接收此触摸
optional func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool
/// 手势是否应该接收此按压(iOS 9+)
optional func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive press: UIPress
) -> Bool
/// 手势是否应该接收此事件(iOS 13.4+)
@available(iOS 13.4, *)
optional func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive event: UIEvent
) -> Bool
}
常见手势冲突解决
swift
// MARK: - 手势冲突解决方案
/*
═══════════════════════════════════════════════════════════════════
场景1:单击与双击冲突
═══════════════════════════════════════════════════════════════════
问题:同时添加单击和双击手势,单击总是先被识别
*/
class TapConflictView: UIView {
let singleTap = UITapGestureRecognizer()
let doubleTap = UITapGestureRecognizer()
func setupGestures() {
singleTap.numberOfTapsRequired = 1
doubleTap.numberOfTapsRequired = 2
// 解决方案:单击需要等待双击失败
singleTap.require(toFail: doubleTap)
addGestureRecognizer(singleTap)
addGestureRecognizer(doubleTap)
}
}
/*
═══════════════════════════════════════════════════════════════════
场景2:ScrollView 中的 Tap 手势
═══════════════════════════════════════════════════════════════════
问题:ScrollView 内的 Tap 手势可能与滚动冲突
*/
class ScrollViewTapView: UIView, UIGestureRecognizerDelegate {
let scrollView = UIScrollView()
let tapGesture = UITapGestureRecognizer()
func setup() {
tapGesture.delegate = self
scrollView.addGestureRecognizer(tapGesture)
}
// 允许 Tap 与 ScrollView 的 Pan 同时识别
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
if gestureRecognizer == tapGesture &&
otherGestureRecognizer is UIPanGestureRecognizer {
return true
}
return false
}
}
/*
═══════════════════════════════════════════════════════════════════
场景3:自定义手势与系统手势冲突(NavigationController 侧滑返回)
═══════════════════════════════════════════════════════════════════
问题:自定义的 Pan 手势影响了系统侧滑返回
*/
class CustomPanViewController: UIViewController, UIGestureRecognizerDelegate {
let customPan = UIPanGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
customPan.delegate = self
view.addGestureRecognizer(customPan)
}
// 让系统侧滑手势优先
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
// 如果是系统的侧滑手势,我们的手势需要等它失败
if let navController = navigationController,
otherGestureRecognizer == navController.interactivePopGestureRecognizer {
return true
}
return false
}
// 或者在边缘区域不响应我们的手势
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool {
let location = touch.location(in: view)
// 左边缘 40pt 范围内不响应
if location.x < 40 {
return false
}
return true
}
}
/*
═══════════════════════════════════════════════════════════════════
场景4:嵌套 ScrollView 滚动冲突
═══════════════════════════════════════════════════════════════════
问题:垂直 ScrollView 内嵌水平 ScrollView,滚动方向判断不准
*/
class NestedScrollView: UIScrollView, UIGestureRecognizerDelegate {
enum ScrollDirection {
case vertical
case horizontal
}
var scrollDirection: ScrollDirection = .vertical
override init(frame: CGRect) {
super.init(frame: frame)
panGestureRecognizer.delegate = self
}
required init?(coder: NSCoder) {
super.init(coder: coder)
panGestureRecognizer.delegate = self
}
// 判断是否应该接收触摸
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
return true
}
let velocity = panGesture.velocity(in: self)
switch scrollDirection {
case .vertical:
// 垂直滚动:只接受垂直方向为主的滑动
return abs(velocity.y) > abs(velocity.x)
case .horizontal:
// 水平滚动:只接受水平方向为主的滑动
return abs(velocity.x) > abs(velocity.y)
}
}
// 允许与其他 ScrollView 同时识别
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
// 允许与父/子 ScrollView 同时滚动
if otherGestureRecognizer.view is UIScrollView {
return true
}
return false
}
}
/*
═══════════════════════════════════════════════════════════════════
场景5:手势与 UIControl 冲突
═══════════════════════════════════════════════════════════════════
问题:View 上的 Tap 手势影响了子视图 Button 的点击
*/
class GestureWithControlView: UIView, UIGestureRecognizerDelegate {
let tapGesture = UITapGestureRecognizer()
let button = UIButton()
func setup() {
tapGesture.delegate = self
addGestureRecognizer(tapGesture)
addSubview(button)
}
// 方案1:排除 UIControl
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool {
// 如果点击到 UIControl,手势不接收
if touch.view is UIControl {
return false
}
return true
}
// 方案2:通过 cancelsTouchesInView
func alternativeSetup() {
tapGesture.cancelsTouchesInView = false // 手势成功后不取消触摸
}
}
自定义手势识别器
swift
// MARK: - 自定义手势识别器
/// 自定义:两指触摸手势
class TwoFingerTouchGestureRecognizer: UIGestureRecognizer {
/// 两指初始中点
private(set) var initialMidpoint: CGPoint = .zero
/// 当前两指中点
private(set) var currentMidpoint: CGPoint = .zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
guard let allTouches = event.allTouches, allTouches.count == 2 else {
state = .failed
return
}
let touchArray = Array(allTouches)
let point1 = touchArray[0].location(in: view)
let point2 = touchArray[1].location(in: view)
initialMidpoint = CGPoint(
x: (point1.x + point2.x) / 2,
y: (point1.y + point2.y) / 2
)
currentMidpoint = initialMidpoint
state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard state == .began || state == .changed,
let allTouches = event.allTouches, allTouches.count == 2 else {
return
}
let touchArray = Array(allTouches)
let point1 = touchArray[0].location(in: view)
let point2 = touchArray[1].location(in: view)
currentMidpoint = CGPoint(
x: (point1.x + point2.x) / 2,
y: (point1.y + point2.y) / 2
)
state = .changed
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
state = .cancelled
}
override func reset() {
super.reset()
initialMidpoint = .zero
currentMidpoint = .zero
}
}
/// 自定义:长按后拖动手势
class LongPressDragGestureRecognizer: UIGestureRecognizer {
/// 长按时长(秒)
var minimumPressDuration: TimeInterval = 0.5
/// 允许的移动距离
var allowableMovement: CGFloat = 10
/// 拖动位置
private(set) var dragLocation: CGPoint = .zero
private var startLocation: CGPoint = .zero
private var longPressTimer: Timer?
private var isLongPressTriggered = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
guard touches.count == 1, let touch = touches.first else {
state = .failed
return
}
startLocation = touch.location(in: view)
isLongPressTriggered = false
// 开始长按计时
longPressTimer = Timer.scheduledTimer(
withTimeInterval: minimumPressDuration,
repeats: false
) { [weak self] _ in
self?.isLongPressTriggered = true
self?.state = .began
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else { return }
let currentLocation = touch.location(in: view)
dragLocation = currentLocation
if !isLongPressTriggered {
// 长按未触发时,检查是否移动超过阈值
let distance = hypot(
currentLocation.x - startLocation.x,
currentLocation.y - startLocation.y
)
if distance > allowableMovement {
longPressTimer?.invalidate()
longPressTimer = nil
state = .failed
}
} else {
// 长按已触发,更新拖动状态
state = .changed
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
longPressTimer?.invalidate()
longPressTimer = nil
if isLongPressTriggered {
state = .ended
} else {
state = .failed
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
longPressTimer?.invalidate()
longPressTimer = nil
state = .cancelled
}
override func reset() {
super.reset()
longPressTimer?.invalidate()
longPressTimer = nil
isLongPressTriggered = false
dragLocation = .zero
startLocation = .zero
}
}
事件拦截与转发实战
全局事件拦截
swift
// MARK: - Application 级别事件拦截
```swift
// MARK: - Application 级别事件拦截
class EventInterceptorApplication: UIApplication {
override func sendEvent(_ event: UIEvent) {
// 在事件分发前拦截
if let touches = event.allTouches {
for touch in touches {
// 记录所有触摸事件(用于统计、调试等)
logTouchEvent(touch, event: event)
// 全局手势检测(如:三指下滑调出调试面板)
if shouldShowDebugPanel(touches: touches) {
showDebugPanel()
return // 拦截事件,不再向下传递
}
}
}
// 继续正常分发
super.sendEvent(event)
}
private func logTouchEvent(_ touch: UITouch, event: UIEvent) {
#if DEBUG
let location = touch.location(in: nil)
let phase = touch.phase
print("📍 Touch: \(location), phase: \(phase)")
#endif
}
private func shouldShowDebugPanel(touches: Set<UITouch>) -> Bool {
#if DEBUG
// 三指同时触摸
return touches.count >= 3
#else
return false
#endif
}
private func showDebugPanel() {
// 显示调试面板
}
}
// main.swift 中使用自定义 UIApplication
// UIApplicationMain(
// CommandLine.argc,
// CommandLine.unsafeArgv,
// NSStringFromClass(EventInterceptorApplication.self),
// NSStringFromClass(AppDelegate.self)
// )
Window 级别事件拦截
swift
// MARK: - Window 级别事件拦截
class EventInterceptorWindow: UIWindow {
/// 事件拦截器列表
private var interceptors: [EventInterceptor] = []
/// 添加拦截器
func addInterceptor(_ interceptor: EventInterceptor) {
interceptors.append(interceptor)
}
override func sendEvent(_ event: UIEvent) {
// 执行拦截器
for interceptor in interceptors {
if interceptor.shouldIntercept(event: event) {
interceptor.handle(event: event)
if interceptor.consumeEvent {
return // 拦截器消费了事件
}
}
}
super.sendEvent(event)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 可以在这里修改 hitTest 行为
let hitView = super.hitTest(point, with: event)
#if DEBUG
print("🎯 HitTest View: \(hitView?.debugDescription ?? "nil")")
#endif
return hitView
}
}
/// 事件拦截器协议
protocol EventInterceptor {
/// 是否应该拦截此事件
func shouldIntercept(event: UIEvent) -> Bool
/// 处理事件
func handle(event: UIEvent)
/// 是否消费事件(消费后不再向下传递)
var consumeEvent: Bool { get }
}
// MARK: - 防重复点击拦截器
class AntiRepeatTapInterceptor: EventInterceptor {
/// 最小点击间隔(秒)
var minimumInterval: TimeInterval = 0.5
/// 上次点击时间
private var lastTapTime: TimeInterval = 0
/// 是否消费事件
var consumeEvent: Bool { return true }
func shouldIntercept(event: UIEvent) -> Bool {
guard event.type == .touches,
let touch = event.allTouches?.first,
touch.phase == .began else {
return false
}
let currentTime = Date().timeIntervalSince1970
let interval = currentTime - lastTapTime
if interval < minimumInterval {
print("⚠️ 点击过快,已拦截")
return true
}
lastTapTime = currentTime
return false
}
func handle(event: UIEvent) {
// 被拦截的事件不做处理
}
}
// MARK: - 用户行为记录拦截器
class UserBehaviorInterceptor: EventInterceptor {
var consumeEvent: Bool { return false } // 不消费,只记录
private var touchSequence: [TouchRecord] = []
struct TouchRecord {
let location: CGPoint
let timestamp: TimeInterval
let phase: UITouch.Phase
let viewClass: String
}
func shouldIntercept(event: UIEvent) -> Bool {
return event.type == .touches
}
func handle(event: UIEvent) {
guard let touches = event.allTouches else { return }
for touch in touches {
let record = TouchRecord(
location: touch.location(in: nil),
timestamp: touch.timestamp,
phase: touch.phase,
viewClass: String(describing: type(of: touch.view ?? UIView.self))
)
touchSequence.append(record)
}
// 定期上报或保存
if touchSequence.count > 100 {
uploadAndClear()
}
}
private func uploadAndClear() {
// 上报用户行为数据
let data = touchSequence
touchSequence.removeAll()
// 异步上报
DispatchQueue.global().async {
// 上报 data
}
}
}
事件穿透与转发
swift
// MARK: - 复杂事件穿透场景
/*
═══════════════════════════════════════════════════════════════════
场景:半透明蒙层,点击蒙层关闭,点击内容正常响应
═══════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│ BackgroundView │
│ ┌───────────────────────────────────────┐ │
│ │ MaskView (半透明蒙层) │ │
│ │ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ ContentView │ │ │
│ │ │ (弹窗内容) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ Button │ │ │ │
│ │ │ └─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │
│ │ ← 点击蒙层区域:关闭弹窗 │ │
│ │ ← 点击内容区域:正常响应 │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
*/
class PopupMaskView: UIView {
/// 内容视图
let contentView = UIView()
/// 点击蒙层关闭回调
var onMaskTapped: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
backgroundColor = UIColor.black.withAlphaComponent(0.5)
addSubview(contentView)
contentView.backgroundColor = .white
contentView.layer.cornerRadius = 12
// 蒙层点击手势
let tap = UITapGestureRecognizer(target: self, action: #selector(maskTapped))
tap.delegate = self
addGestureRecognizer(tap)
}
@objc private func maskTapped() {
onMaskTapped?()
}
// 关键:让内容区域正常响应
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
// 如果点击到内容视图或其子视图,正常返回
if let hitView = hitView, hitView.isDescendant(of: contentView) {
return hitView
}
// 点击蒙层区域,返回自己(触发手势)
if bounds.contains(point) {
return self
}
return nil
}
}
extension PopupMaskView: UIGestureRecognizerDelegate {
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool {
// 只有点击蒙层区域才响应手势
let location = touch.location(in: self)
return !contentView.frame.contains(location)
}
}
// MARK: - 事件转发到另一个视图
class EventForwardingView: UIView {
/// 事件转发目标
weak var forwardingTarget: UIView?
/// 是否同时响应自身和转发
var simultaneousResponse = false
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 检查是否应该转发
if let target = forwardingTarget {
let targetPoint = convert(point, to: target)
if let targetHitView = target.hitTest(targetPoint, with: event) {
// 找到转发目标的响应视图
if simultaneousResponse {
// 同时响应:返回自身,但在触摸方法中转发
return self
} else {
// 只转发:返回目标视图
return targetHitView
}
}
}
return super.hitTest(point, with: event)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if simultaneousResponse {
// 转发给目标
forwardingTarget?.touchesBegan(touches, with: event)
}
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if simultaneousResponse {
forwardingTarget?.touchesMoved(touches, with: event)
}
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if simultaneousResponse {
forwardingTarget?.touchesEnded(touches, with: event)
}
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if simultaneousResponse {
forwardingTarget?.touchesCancelled(touches, with: event)
}
super.touchesCancelled(touches, with: event)
}
}
// MARK: - 多视图同步触摸
/// 触摸同步管理器
class TouchSyncManager {
static let shared = TouchSyncManager()
/// 同步组:组内视图共享触摸事件
private var syncGroups: [String: [WeakViewWrapper]] = [:]
private init() {}
/// 添加视图到同步组
func addView(_ view: UIView, to group: String) {
if syncGroups[group] == nil {
syncGroups[group] = []
}
syncGroups[group]?.append(WeakViewWrapper(view: view))
}
/// 从同步组移除视图
func removeView(_ view: UIView, from group: String) {
syncGroups[group]?.removeAll { $0.view === view }
}
/// 同步触摸事件到组内其他视图
func syncTouches(
_ touches: Set<UITouch>,
with event: UIEvent?,
from sourceView: UIView,
in group: String,
phase: UITouch.Phase
) {
guard let views = syncGroups[group] else { return }
for wrapper in views {
guard let targetView = wrapper.view,
targetView !== sourceView else { continue }
// 转换触摸坐标
// 注意:这里只是模拟同步,实际触摸对象无法完全复制
switch phase {
case .began:
targetView.touchesBegan(touches, with: event)
case .moved:
targetView.touchesMoved(touches, with: event)
case .ended:
targetView.touchesEnded(touches, with: event)
case .cancelled:
targetView.touchesCancelled(touches, with: event)
default:
break
}
}
}
// 弱引用包装
private class WeakViewWrapper {
weak var view: UIView?
init(view: UIView) {
self.view = view
}
}
}
复杂场景解决方案
场景1:卡片堆叠滑动
swift
// MARK: - 卡片堆叠滑动(类似 Tinder)
/*
═══════════════════════════════════════════════════════════════════
┌─────────────────────────────────────┐
│ CardStackView │
│ ┌─────────────────────────────┐ │
│ │ Card 3 (top) │ │ ← 顶部卡片可拖动
│ │ ┌─────────────────────┐ │ │
│ │ │ Card 2 │ │ │ ← 第二张卡片可见但不响应
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ Card 1 │ │ │ │ ← 底部卡片
│ │ │ └───────────────┘ │ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════
*/
class CardStackView: UIView {
/// 所有卡片
private var cards: [CardView] = []
/// 当前顶部卡片
private var topCard: CardView? {
return cards.last
}
/// 卡片被滑走回调
var onCardSwiped: ((CardView, SwipeDirection) -> Void)?
enum SwipeDirection {
case left, right, up, down
}
// 只让顶部卡片响应触摸
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let topCard = topCard else { return nil }
let cardPoint = convert(point, to: topCard)
// 只对顶部卡片进行 hitTest
if let hitView = topCard.hitTest(cardPoint, with: event) {
return hitView
}
return nil
}
func addCard(_ card: CardView) {
cards.append(card)
insertSubview(card, at: 0) // 新卡片放在底部
// 添加拖动手势
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
card.addGestureRecognizer(pan)
updateCardPositions()
}
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let card = gesture.view as? CardView else { return }
let translation = gesture.translation(in: self)
let velocity = gesture.velocity(in: self)
switch gesture.state {
case .changed:
// 跟随手指移动
card.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
.rotated(by: translation.x / 1000) // 轻微旋转
case .ended:
let shouldSwipe = abs(translation.x) > 100 || abs(velocity.x) > 500
if shouldSwipe {
let direction: SwipeDirection = translation.x > 0 ? .right : .left
swipeCard(card, direction: direction)
} else {
// 回弹
UIView.animate(withDuration: 0.3) {
card.transform = .identity
}
}
default:
break
}
}
private func swipeCard(_ card: CardView, direction: SwipeDirection) {
let offscreenX: CGFloat = direction == .right ? 500 : -500
UIView.animate(withDuration: 0.3, animations: {
card.transform = CGAffineTransform(translationX: offscreenX, y: 0)
card.alpha = 0
}) { _ in
card.removeFromSuperview()
self.cards.removeAll { $0 === card }
self.onCardSwiped?(card, direction)
}
}
private func updateCardPositions() {
for (index, card) in cards.enumerated() {
let offset = CGFloat(cards.count - 1 - index) * 8
card.transform = CGAffineTransform(translationX: 0, y: offset)
.scaledBy(x: 1 - CGFloat(cards.count - 1 - index) * 0.05, y: 1)
}
}
}
class CardView: UIView {
// 卡片内容...
}
场景2:画中画拖动
swift
// MARK: - 画中画可拖动视图
/*
═══════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ Main Content │
│ │
│ │
│ ┌───────────────────┐ │
│ │ PiP Window │ ← 可自由拖动 │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ Button │ │ ← 内部按钮可点击 │
│ │ └───────────┘ │ │
│ │ │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════
*/
class PictureInPictureView: UIView {
/// 拖动区域(如顶部标题栏)
let dragHandle = UIView()
/// 内容区域
let contentView = UIView()
/// 吸附到边缘
var snapToEdges = true
/// 边缘间距
var edgePadding: CGFloat = 8
private var initialCenter: CGPoint = .zero
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
layer.cornerRadius = 12
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 4)
layer.shadowRadius = 8
layer.shadowOpacity = 0.3
// 拖动区域
dragHandle.backgroundColor = UIColor.systemGray5
addSubview(dragHandle)
// 内容区域
contentView.backgroundColor = .white
addSubview(contentView)
// 拖动手势只添加到拖动区域
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
dragHandle.addGestureRecognizer(pan)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 先检查内容区域的子视图(如按钮)
let contentPoint = convert(point, to: contentView)
if let hitView = contentView.hitTest(contentPoint, with: event),
hitView !== contentView {
return hitView // 返回内容区域中的按钮等
}
// 检查拖动区域
let handlePoint = convert(point, to: dragHandle)
if dragHandle.point(inside: handlePoint, with: event) {
return dragHandle // 返回拖动区域,触发拖动手势
}
// 其他区域返回自身
if bounds.contains(point) {
return self
}
return nil
}
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let superview = superview else { return }
switch gesture.state {
case .began:
initialCenter = center
// 轻微放大效果
UIView.animate(withDuration: 0.2) {
self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
}
case .changed:
let translation = gesture.translation(in: superview)
center = CGPoint(
x: initialCenter.x + translation.x,
y: initialCenter.y + translation.y
)
case .ended, .cancelled:
// 恢复大小
UIView.animate(withDuration: 0.2) {
self.transform = .identity
}
if snapToEdges {
snapToNearestEdge(in: superview)
}
default:
break
}
}
private func snapToNearestEdge(in superview: UIView) {
let superBounds = superview.bounds
var targetCenter = center
// 限制在父视图内
let halfWidth = bounds.width / 2
let halfHeight = bounds.height / 2
targetCenter.x = max(halfWidth + edgePadding,
min(superBounds.width - halfWidth - edgePadding, targetCenter.x))
targetCenter.y = max(halfHeight + edgePadding,
min(superBounds.height - halfHeight - edgePadding, targetCenter.y))
// 吸附到最近的边缘
let distanceToLeft = targetCenter.x - halfWidth
let distanceToRight = superBounds.width - targetCenter.x - halfWidth
if distanceToLeft < distanceToRight {
targetCenter.x = halfWidth + edgePadding
} else {
targetCenter.x = superBounds.width - halfWidth - edgePadding
}
UIView.animate(
withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0,
options: [],
animations: {
self.center = targetCenter
}
)
}
}
场景3:多层 ScrollView 嵌套
swift
// MARK: - 多层 ScrollView 嵌套(首页复杂布局)
/*
═══════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ OuterScrollView (垂直滚动) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Header (固定内容) │ │
│ ├───────────────────────────────────────────────────────────┤ │
│ │ HorizontalScrollView (水平滚动) │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │Card1│ │Card2│ │Card3│ │Card4│ →→→ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ ├───────────────────────────────────────────────────────────┤ │
│ │ NestedTableView (垂直滚动,与外层联动) │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ Cell 1 │ │ │
│ │ ├───────────────────────────────────────────────────┤ │ │
│ │ │ Cell 2 │ │ │
│ │ ├───────────────────────────────────────────────────┤ │ │
│ │ │ Cell 3 │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
滚动逻辑:
1. 向上滑动时,先滚动外层 ScrollView,直到 Header 消失
2. Header 消失后,滚动内层 TableView
3. 向下滑动时,先滚动内层,直到内层到顶
4. 内层到顶后,滚动外层,Header 出现
═══════════════════════════════════════════════════════════════════
*/
class NestedScrollViewController: UIViewController {
/// 外层 ScrollView
private let outerScrollView = UIScrollView()
/// Header 视图
private let headerView = UIView()
/// 水平滚动视图
private let horizontalScrollView = UIScrollView()
/// 内层 TableView
private let innerTableView = UITableView()
/// Header 高度
private let headerHeight: CGFloat = 200
/// 是否应该滚动外层
private var shouldScrollOuter = true
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
private func setupViews() {
// 外层 ScrollView 设置
outerScrollView.delegate = self
outerScrollView.showsVerticalScrollIndicator = false
view.addSubview(outerScrollView)
// Header
headerView.backgroundColor = .systemBlue
outerScrollView.addSubview(headerView)
// 水平滚动
horizontalScrollView.showsHorizontalScrollIndicator = false
outerScrollView.addSubview(horizontalScrollView)
// 内层 TableView
innerTableView.delegate = self
innerTableView.dataSource = self
innerTableView.isScrollEnabled = false // 初始禁用滚动
outerScrollView.addSubview(innerTableView)
setupLayout()
}
private func setupLayout() {
// 布局代码...
outerScrollView.frame = view.bounds
headerView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: headerHeight)
// ...
}
}
// MARK: - UIScrollViewDelegate
extension NestedScrollViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView === outerScrollView {
handleOuterScroll()
} else if scrollView === innerTableView {
handleInnerScroll()
}
}
private func handleOuterScroll() {
let offsetY = outerScrollView.contentOffset.y
// Header 滚出屏幕
if offsetY >= headerHeight {
// 固定外层位置,开始滚动内层
outerScrollView.contentOffset.y = headerHeight
innerTableView.isScrollEnabled = true
shouldScrollOuter = false
} else {
innerTableView.isScrollEnabled = false
shouldScrollOuter = true
}
}
private func handleInnerScroll() {
let offsetY = innerTableView.contentOffset.y
// 内层滚动到顶部
if offsetY <= 0 {
innerTableView.contentOffset.y = 0
innerTableView.isScrollEnabled = false
shouldScrollOuter = true
}
}
}
// MARK: - 手势协调
extension NestedScrollViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
// 允许外层和内层同时识别手势
return true
}
}
extension NestedScrollViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
}
场景4:手写画板
swift
// MARK: - 手写画板(精确触摸追踪)
class DrawingCanvasView: UIView {
/// 绘制路径
private var paths: [(path: UIBezierPath, color: UIColor, width: CGFloat)] = []
/// 当前路径
private var currentPath: UIBezierPath?
/// 画笔颜色
var strokeColor: UIColor = .black
/// 画笔宽度
var strokeWidth: CGFloat = 3.0
/// 是否启用压感
var pressureSensitive = true
/// 合并触摸点(提高性能)
private var coalescedTouches: [UITouch] = []
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
isMultipleTouchEnabled = false // 单指绘制
}
required init?(coder: NSCoder) {
super.init(coder: coder)
backgroundColor = .white
isMultipleTouchEnabled = false
}
// MARK: - 触摸处理
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let path = UIBezierPath()
path.lineCapStyle = .round
path.lineJoinStyle = .round
let point = touch.location(in: self)
path.move(to: point)
currentPath = path
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first,
let path = currentPath else { return }
// 使用合并触摸提高精度(iOS 9+)
if let coalescedTouches = event?.coalescedTouches(for: touch) {
for coalescedTouch in coalescedTouches {
let point = coalescedTouch.location(in: self)
path.addLine(to: point)
}
} else {
let point = touch.location(in: self)
path.addLine(to: point)
}
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let path = currentPath else { return }
// 保存路径
paths.append((path: path, color: strokeColor, width: strokeWidth))
currentPath = nil
setNeedsDisplay()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
// 取消时丢弃当前路径
currentPath = nil
setNeedsDisplay()
}
// MARK: - 预测触摸(减少延迟)
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first,
let path = currentPath else { return }
// 合并触摸(已发生的)
if let coalescedTouches = event?.coalescedTouches(for: touch) {
for coalescedTouch in coalescedTouches {
addPoint(from: coalescedTouch, to: path)
}
}
// 预测触摸(尚未发生,用于减少视觉延迟)
if let predictedTouches = event?.predictedTouches(for: touch) {
// 预测的点只用于绘制,不保存
for predictedTouch in predictedTouches {
let point = predictedTouch.location(in: self)
// 用虚线或半透明绘制预测路径
}
}
setNeedsDisplay()
}
private func addPoint(from touch: UITouch, to path: UIBezierPath) {
let point = touch.location(in: self)
if pressureSensitive {
// 根据压力调整线宽
let force = touch.force / touch.maximumPossibleForce
let adjustedWidth = strokeWidth * (0.5 + force * 0.5)
path.lineWidth = adjustedWidth
}
path.addLine(to: point)
}
// MARK: - 绘制
override func draw(_ rect: CGRect) {
// 绘制已保存的路径
for (path, color, width) in paths {
color.setStroke()
path.lineWidth = width
path.stroke()
}
// 绘制当前路径
if let path = currentPath {
strokeColor.setStroke()
path.lineWidth = strokeWidth
path.stroke()
}
}
// MARK: - 操作
func clear() {
paths.removeAll()
currentPath = nil
setNeedsDisplay()
}
func undo() {
guard !paths.isEmpty else { return }
paths.removeLast()
setNeedsDisplay()
}
}
// MARK: - Apple Pencil 支持
extension DrawingCanvasView {
/// 处理 Apple Pencil 专属特性
private func handleApplePencil(touch: UITouch) {
guard touch.type == .pencil else { return }
// 倾斜角度
let altitudeAngle = touch.altitudeAngle // 0 = 平行,π/2 = 垂直
// 方位角
let azimuthAngle = touch.azimuthAngle(in: self)
// 方位向量
let azimuthVector = touch.azimuthUnitVector(in: self)
// 可以根据这些值调整笔触效果
// 例如:倾斜时画出阴影效果
}
}
调试与性能优化
Hit-Test 调试工具
swift
// MARK: - Hit-Test 调试器
#if DEBUG
class HitTestDebugger {
static let shared = HitTestDebugger()
/// 是否启用调试
var isEnabled = false
/// 高亮显示的视图
private var highlightView: UIView?
/// 调试信息 Label
private var infoLabel: UILabel?
private init() {}
/// 高亮 Hit-Test 视图
func highlight(view: UIView?) {
guard isEnabled, let view = view else {
highlightView?.removeFromSuperview()
highlightView = nil
return
}
if highlightView == nil {
highlightView = UIView()
highlightView?.backgroundColor = UIColor.red.withAlphaComponent(0.3)
highlightView?.layer.borderColor = UIColor.red.cgColor
highlightView?.layer.borderWidth = 2
highlightView?.isUserInteractionEnabled = false
}
guard let highlight = highlightView,
let window = view.window else { return }
let frameInWindow = view.convert(view.bounds, to: window)
highlight.frame = frameInWindow
window.addSubview(highlight)
// 显示视图信息
showInfo(for: view, in: window)
}
private func showInfo(for view: UIView, in window: UIWindow) {
if infoLabel == nil {
infoLabel = UILabel()
infoLabel?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
infoLabel?.textColor = .white
infoLabel?.font = .systemFont(ofSize: 10)
infoLabel?.numberOfLines = 0
infoLabel?.layer.cornerRadius = 4
infoLabel?.clipsToBounds = true
infoLabel?.textAlignment = .left
}
guard let label = infoLabel else { return }
let info = """
Class: \(type(of: view))
Frame: \(view.frame)
Alpha: \(view.alpha)
Hidden: \(view.isHidden)
UserInteraction: \(view.isUserInteractionEnabled)
"""
label.text = " " + info.replacingOccurrences(of: "\n", with: "\n ") + " "
label.sizeToFit()
label.frame.origin = CGPoint(x: 10, y: window.safeAreaInsets.top + 10)
window.addSubview(label)
}
/// 打印视图层级
func printViewHierarchy(from view: UIView, indent: Int = 0) {
let indentString = String(repeating: " ", count: indent)
let viewInfo = "\(indentString)├─ \(type(of: view)): \(view.frame)"
print(viewInfo)
for subview in view.subviews {
printViewHierarchy(from: subview, indent: indent + 1)
}
}
}
// MARK: - UIView 调试扩展
extension UIView {
/// 调试用:标记 hitTest 调用
func debugHitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = hitTest(point, with: event)
#if DEBUG
print("""
🔍 [HitTest] \(type(of: self))
Point: \(point)
Result: \(result.map { String(describing: type(of: $0)) } ?? "nil")
""")
#endif
return result
}
/// 调试用:打印响应者链
func debugResponderChain() {
#if DEBUG
var responder: UIResponder? = self
var chain: [String] = []
while let current = responder {
chain.append(String(describing: type(of: current)))
responder = current.next
}
print("═══════════════════════════════════════")
print("📍 Responder Chain from \(type(of: self)):")
for (index, name) in chain.enumerated() {
print(" \(index). \(name)")
}
print("═══════════════════════════════════════")
#endif
}
}
#endif
性能优化
swift
// MARK: - 触摸事件性能优化
/*
═══════════════════════════════════════════════════════════════════
触摸事件性能优化建议
═══════════════════════════════════════════════════════════════════
1. 减少视图层级
- hitTest 会遍历所有子视图
- 层级越深,性能损耗越大
2. 合理使用 userInteractionEnabled
- 不需要交互的视图设为 false
- 减少 hitTest 遍历范围
3. 避免在触摸方法中做耗时操作
- 触摸事件在主线程
- 耗时操作会导致卡顿
4. 使用 CADisplayLink 处理连续触摸
- 减少不必要的重绘
- 与屏幕刷新同步
5. 合并触摸点
- 使用 coalescedTouches 获取完整轨迹
- 使用 predictedTouches 减少延迟
═══════════════════════════════════════════════════════════════════
*/
// MARK: - 优化的绘图视图
class OptimizedDrawingView: UIView {
/// CADisplayLink
private var displayLink: CADisplayLink?
/// 待处理的触摸点
private var pendingPoints: [CGPoint] = []
/// 线条图层(用于硬件加速)
private let shapeLayer = CAShapeLayer()
/// 当前路径
private let currentPath = UIBezierPath()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
// 使用 CAShapeLayer 代替 drawRect(性能更好)
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = nil
shapeLayer.lineWidth = 3
shapeLayer.lineCap = .round
shapeLayer.lineJoin = .round
layer.addSublayer(shapeLayer)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let point = touch.location(in: self)
currentPath.move(to: point)
startDisplayLink()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first,
let coalescedTouches = event?.coalescedTouches(for: touch) else { return }
// 收集所有触摸点,但不立即处理
for coalescedTouch in coalescedTouches {
let point = coalescedTouch.location(in: self)
pendingPoints.append(point)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
processPendingPoints()
stopDisplayLink()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
pendingPoints.removeAll()
stopDisplayLink()
}
// MARK: - Display Link
private func startDisplayLink() {
guard displayLink == nil else { return }
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkFired))
displayLink?.add(to: .main, forMode: .common)
}
private func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
@objc private func displayLinkFired() {
processPendingPoints()
}
private func processPendingPoints() {
guard !pendingPoints.isEmpty else { return }
// 批量处理触摸点
for point in pendingPoints {
currentPath.addLine(to: point)
}
pendingPoints.removeAll()
// 更新图层路径(在主线程,但是 GPU 加速)
shapeLayer.path = currentPath.cgPath
}
deinit {
stopDisplayLink()
}
}
// MARK: - hitTest 缓存优化
class CachedHitTestView: UIView {
/// hitTest 缓存
private var hitTestCache: [CGPoint: UIView?] = [:]
/// 缓存有效区域大小
private let cacheGridSize: CGFloat = 10
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 将点量化到网格
let gridPoint = CGPoint(
x: floor(point.x / cacheGridSize) * cacheGridSize,
y: floor(point.y / cacheGridSize) * cacheGridSize
)
// 检查缓存
if let cached = hitTestCache[gridPoint] {
return cached
}
// 执行 hitTest
let result = super.hitTest(point, with: event)
// 缓存结果
hitTestCache[gridPoint] = result
return result
}
/// 布局变化时清除缓存
override func layoutSubviews() {
super.layoutSubviews()
hitTestCache.removeAll()
}
/// 子视图变化时清除缓存
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
hitTestCache.removeAll()
}
override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
hitTestCache.removeAll()
}
}
总结
┌─────────────────────────────────────────────────────────────────────────┐
│ iOS 触摸事件传递核心要点 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 📱 事件传递流程 │
│ 硬件 → IOKit → SpringBoard → App RunLoop → UIApplication → │
│ UIWindow → Hit-Test → UIView │
│ │
│ 🎯 Hit-Test 机制 │
│ • 从 Window 开始,递归遍历子视图 │
│ • 倒序遍历(后添加的视图优先) │
│ • 检查三个条件:显示、交互、alpha │
│ • pointInside 判断点是否在范围内 │
│ │
│ ⛓️ 响应者链 │
│ • 与 Hit-Test 方向相反 │
│ • View → SuperView → ViewController → Window → Application │
│ • 事件沿链传递,直到被处理或丢弃 │
│ │
│ 🤚 手势识别器 │
│ • 与触摸方法并行接收事件 │
│ • 默认成功后取消触摸方法 │
│ • 通过 Delegate 解决冲突 │
│ │
│ ⚡ 常用技巧 │
│ • 扩大点击区域:重写 pointInside │
│ • 事件穿透:hitTest 返回 nil │
│ • 超出父视图响应:父视图重写 hitTest │
│ • 手势冲突:require(toFail:) 或 Delegate 方法 │
│ │
│ 🔧 调试优化 │
│ • 减少视图层级 │
│ • 合理设置 userInteractionEnabled │
│ • 使用 CADisplayLink 同步绘制 │
│ • 使用 coalescedTouches 获取完整轨迹 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
触摸事件传递是 iOS 交互的基础,理解整个流程对于解决复杂交互问题、优化用户体验至关重要。掌握 Hit-Test、响应者链、手势识别器三者的关系和协作机制,才能游刃有余地处理各种交互场景。