iOS 触摸事件完整传递链路:Hit-Test 全流程深度解析

触摸事件概述

事件类型

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                      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、响应者链、手势识别器三者的关系和协作机制,才能游刃有余地处理各种交互场景。

相关推荐
Swift社区4 小时前
用 Task Local Values 构建 Swift 里的依赖容器:一种更轻量的依赖注入思路
开发语言·ios·swift
2501_915909064 小时前
苹果应用加密方案的一种方法,在没有源码的前提下,如何处理 IPA 的安全问题
android·安全·ios·小程序·uni-app·iphone·webview
TouchWorld4 小时前
iOS逆向-哔哩哔哩增加3倍速播放(4)- 竖屏视频·全屏播放场景
ios·swift
2501_915909064 小时前
iOS 项目中常被忽略的 Bundle ID 管理问题
android·ios·小程序·https·uni-app·iphone·webview
2501_915921434 小时前
iOS App 测试的工程化实践,多工具协同的一些尝试
android·ios·小程序·https·uni-app·iphone·webview
denggun123455 小时前
ios卡顿监测和优化(二)
ios
ChineHe5 小时前
Gin框架入门篇002_第一个Gin服务
macos·xcode·gin
Roc.Chang5 小时前
解决 macOS 26.1 The application “xxxx” can’t be opened. 问题
macos