当前位置:首页>iOSAPP>iOS 中的交互事件传递

iOS 中的交互事件传递

  • 2026-10-11 05:22:08
iOS 中的交互事件传递

引子

hitTest、响应链 touches、UIGestureRecognizer、UIControl、ScrollView 抢触摸——其实是一条流水线 + 手势旁路观察网。

触摸落下 → hitTest → 投递(响应链 ∥ 祖先 GR)→ UIControl 等语义层

一、响应链 —— 命中之后往上传

响应链不是 hitTest 的链,hitTest走的是视图树。点在任意 view 上,响应链只从命中 view 沿 superview 往上,永远不扫兄弟节点。所以是先走视图树,再有响应链。

链上全是 UIResponder:UIView、UIViewController、UIApplication。UIGestureRecognizer 不是 UIResponder,不在响应链上。

之后都用GR表示GestureRecognizer。

某一环 override 了 touch 且不调 super,链在此截断。

touches 是 UIResponder 实例方法,不是某个protocol定义的。

在 UIResponder 的 touch 方法里,默认实现会把事件继续传给 next responder。通常是父 view / VC。

二、UIControl —— touch 之上的语义

UIControl 其实是系统实现了 override touches + target-action。

独占交互(如:按钮、画板)通常不调 super;

附加逻辑(如:日志)可处理后再 super。

不调 super 只能挡响应链上传,挡不住 ScrollView 的 pan GR。

三、手势 —— 沿 superview 旁路观察

GR不是Responder,不实现touches;系统把同一次 touch 并行喂给 GR 的状态机,识别成功后再影响 view(delay / cancel)。

点在 ScrollView 里的 Button 上时,系统从 Button 沿 superview 向上,在响应链里收集沿途各层 view 上的 GR,放入观察者列表,登记为本次 touch 的接收者。这些观察者会并行收到touch投递。

ScrollView 的 pan 能收到子 Button 的 touch,走的是视图祖先路径,不是响应链的 next。

观察者列表不属于某个 view 的公开属性,是 UIKit 为每个 UITouch 维护的内部投递状态。

并行投递不是多线程,而是同一次 touch 在主线程里同时走两条路:touch.view 的 touches(响应链)+ 所有登记 GR 的状态机。GR 识别成功且 cancelsTouchesInView 为 true 时,view 收 touchesCancelled。

四、响应链:touch 何时往上传?

不是自动冒泡,而是每一环 touches 里调了 super 才上传。

UIResponder 默认转发给next;

UIView 默认调用 super;

你 override 后由你决定。

每个阶段独立,Began、Moved、Ended、Cancelled 各决定各的。

默认上传:UIView 没 override;或你调了 super。

默认不上传:UIControl 不调 super。

最后:不调 super 是挡不住祖先手势识别的。

五、A→B→C 与手势收集

A View(tap)
    → B View(pan)
        → C View(touch.view)
手势识别 沿 C→B→A 向上收集:B、A 的 GR 都会收到;兄弟 GR 收不到。同时收到更新,默认不能同时 recognized。

六、如何让 C 优先级更高?

只让 C 不 super 不够,只能挡响应链。

  • 做法一:
    C 上挂 tap/pan,B.pan.require(toFail: cGesture)
// C 上:点击 / 拖动手势let cTap = UITapGestureRecognizer(target: self, action: #selector(onCTap))C.addGestureRecognizer(cTap)// B 的 pan 必须等 C 的 tap 判定失败后才能成功B.panGesture.require(toFail: cTap)// 若 C 上要拖动绘制,用 pan:let cPan = UIPanGestureRecognizer(target: self, action: #selector(onCPan))C.addGestureRecognizer(cPan)B.panGesture.require(toFail: cPan)A.tapGesture.require(toFail: cPan) // 若 A 的 tap 也会抢
  • 做法二:
    父 GR delegate shouldReceive 方法中对 touch.view === C 返回 false
// B/A中实现这个方法func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,                       shouldReceive touch: UITouch) -> Bool {    // touch.view 是 C,或点在 C 子树内且应由 C 处理    if touch.view === C || touch.view?.isDescendant(of: C) == true {        return false  // B/A 的手势根本不参与这次 touch    }    return true}
  • 做法三:gestureRecognizerShouldBegin
     在 C 区域否决父 pan
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {    guard let pan = gestureRecognizer as? UIPanGestureRecognizer,          pan.view === B else { return true }    let loc = pan.location(in: C)    if C.bounds.contains(loc) {        return false  // 在 C 区域内,B 的 pan 不开始    }    return true}

七、cancelsTouchesInView相似相关的配置

在创建 GR/ScrollView 时设置,UIKit 在 sendEvent 投递时内部读取,不是你手动调用。

cancelsTouchesInView
delaysTouchesBegan
delaysContentTouches
属于
UIGestureRecognizer
UIGestureRecognizer
UIScrollView
作用
recognized 后 cancel 子树 touch
推迟所附 view 子树 began
推迟子控件 began
会 cancel 吗
会
不会
不会

对手势识别子树含 UIControl例如UIButton 等无豁免权,例如ScrollView 包着 Button,点的是 Button:

  • hit-test 命中的是 Button
  • 但 ScrollView 上的 pan GR 照样进候选集,照样可能 delaysTouchesBegan / cancelsTouchesInView
  • 不会因为子树里是 UIControl 就跳过这套机制

两个 delay 可叠加(began 更晚、用户操作的响应更慢),不会导致 cancel。

cancel 只来自 cancelsTouchesInView,在 gesture recognized 时触发。同一次触摸可先 delay 再 cancel(先等、再滑),是两件独立的事。

最新文章

随机文章