关于TapGesture、UIResponder 链和 target-action 事件的相互影响的机理和应用

https://github.com/hite/IOSEventTest

这是一个供探索、验证、对比测试而精心设计的demo。
用来研究 UIControl,普通的 View 在 使用 target-action, addTapGestureRecognizer 方式时,不同情况下如何相互作用。
试图解答以下问题;

  1. 第一响应者如何确认?(包括 alpha,hidden,clipToBound 属性的影响,都不是本次试验的重点)
  2. target-action 和 tapGesture 混用时,如何表现,加入变量 cancelsTouchesInView 时,是否有不同?
  3. target-action 或者 tapGesture 是否各自都有多个?
    1. 包括相同类型 event state 和 gesture Type 是否有多个?
    2. 包括不同类型 event state 和 gesture Type 是否有多个?
  4. 相同和不同类型 target-action 或者 tapGesture 的添加顺序和执行顺序是否有关系?

设计可测试变量

  1. touch 事件是否冒泡(bubble up)
  2. 是否有是普通 UIResponder 还是 UIControl
  3. 是否添加了 TapGesture 事件。
    这些大测试变量是通过选不同测试入口的方式来进行的。同时,每一个大测试变量里还有一些小测试变量,写在代码内部,个别需要去掉注释或者调整代码顺序来自行验证。如,
  4. 多个相同元素
  5. 多个不同元素
  6. 元素之前的顺序
  7. cancelsTouchesInView 变量(不考虑,delaysTouchesBegan,默认为 NO;delaysTouchesEnded,默认为 YES)

如果没有条件运行源码自己观察,下面是我自己的观察和总结,可能有谬误,欢迎指正
-------TLTR;--------

1. HitTestTouchViewController, 测试普通的 UILabel 元素在是否事件冒泡到父元素的表现

场景1:只有 touch 事件,没有添加 tap 手势,不 bubble 时。

  1. 元素通过 hittest 测试,找到第一响应者后,事件不会继续向下传递。因为第一响应获取之后没有传递
  2. 可以正确 touchesEnded;

场景2:只有 touch 事件,没有添加 tap 手势,容许 bubble 时。

  1. 元素通过 hittest 测试,找到第一响应者后,事件还会继续向上传递事件,按照从子到父的过程,传播事件
  2. 可以正确 touchesEnded;

HitTestTouchGestureController,测试普通的 UILabel 元素在响应 touch 事件的同时,还被添加的 tapGesture 的表现

情况1:普通的 UIResponder 元素, 所有元素都有 touch 事件也有 gesture, touch 事件不 bubble 的情况下

  1. UIApplication 接收到时间,通过 hittest 找到第一响应者之后,先执行 touchesBegan ,然后调用 gesture,然后根据 cancelsTouchesInView 的值,决定是 touchCancelled 还是 touchesEnded。
  2. 无论 cancel 或者 end,此时事件都不会向父元素传播。

情况2:普通的 UIResponder 元素, 所有元素都有 touch 事件也有 gesture, touch 事件容许 bubble 的情况下

  1. UIApplication 接收到时间,通过 hittest 找到第一响应者之后,先执行 touchesBegan,然后调用 gesture
    1. cancelsTouchesInView = NO, 执行 touchesEnded。同时,执行父类的 touchesBegan 和 touchesEnded, 但是不会执行父类的 gesture
    2. cancelsTouchesInView = YES,默认值。执行 touchCancelled。同时,执行父类的 touchesBegan 和 touchesCancelled, 但是不会执行父元素的 gesture
      结论:容许 bubble 的情况下都可以传播到父元素(这是显而易见的,也是这个实验设计的缺陷,在 touchesEnded 里去调用 super 的 touchesEnded 是不是真的符合实际情况?),cancelsTouchesInView 只影响 touch 结束的事件。
      但父元素的 TapGesture 不会响应

HitTestButtonViewController,测试 UIButton 有 addTarget,无 gesture 手势时,是否 bubble up 对 target-action 的影响

多个不同元素、多个相同元素、元素的顺序,元素属性修改是否有影响
情况1:普通的 UIControl 元素, 所有元素都有 touch 事件,有 action 回调,没有 gesture, touch 事件不 bubble 的情况下

  1. UIApplication 接收到时间,通过 hittest 找到第一响应者之后,先执行 touchesBegan,再执行 touchesEnded。但是不会执行 action 回调(why?)
  2. 猜测原因:super 的 touchesEnded:withEvent: 会调用触发 UIControlEventTouchUpInside,因为我们没有调用 super 所以没有触发。
    有调用栈为参考,
    调用栈

    可见,触发 action-target 都是由 UIControl 父类实现的。

情况2:普通的 UIControl 元素, 所有元素都有 touch 事件,有 action 回调,没有 gesture, touch 事件容许 bubble 的情况下

  1. UIApplication 接收到时间,通过 hittest 找到第一响应者之后,先执行 action 回调(allTouchEvent,touchDown 等)、touchesBegan,再执行 action 回调(touchUpInside之类)、touchesEndeded
  2. touch 事件不会传播到父元素,即使是没有 action 回调, button 上的事件也不会向父元素传播。

HitTestButtonGestureController,测试 UIButton 有 addTarget,同时也有 gesture 手势时,是否 bubble up 对 target-action 和 gesture 的影响

情况1:普通的 UIControl 元素, 所有元素都有 touch 事件,有 action 回调,而且有 gesture, touch 事件不 bubble 的情况下

  1. UIApplication 接收到时间,通过 hittest 找到第一响应者之后,先执行 touchesBegan,接下来执行最后一个 gesture 手势,
    1. cancelsTouchesInView=NO 时,再执行 touchesEnded,
    2. cancelsTouchesInView=YES 时,再执行 touchesCancelled
  2. 不会执行 action 回调,因为没有调用 touchesEnded super 方法。

If a gesture recognizer recognizes its gesture, it unbinds the remaining touches of that gesture from their view (so the window won’t deliver them). The window cancels the previously delivered touches with a (touchesCancelled(_:with:)) message. If a gesture recognizer doesn’t recognize its gesture, the view receives all touches in the multi-touch sequence.

情况2:普通的 UIControl 元素, 所有元素都有 touch 事件,有 action 回调,而且有 gesture, touch 事件需要 bubble 的情况下

  1. UIApplication 接收到时间,通过 hittest 找到第一响应者之后
    1. cancelsTouchesInView=NO 时,先执行 action 回调(allTouchEvent,touchDown 等)、touchesBegan,接着是执行 tap 手势响应(重点),再执行 action 回调(touchUpInside之类)、touchesEnded
    2. cancelsTouchesInView=YES 时,先执行 touchesBegan,接下来执行最后一个 gesture 手势,再执行 touchesCancelled
  2. touch 事件不会传播到父元素, gesture 更不会传播到父元素

结论:

  1. 一个元素只能有一个 某某类型的 gesture,后来者覆盖前者;但是可以有多个不同类型的 gesture。
  2. 相同的 UIControl 的 target-event 可以有多个,不弄是否有相同 event state 类型。
  3. 相同阶段的事件,执行顺序按照添加的顺序。
  4. UIControl 内部的事件不会传播到父元素,gesture 也一样。
  5. gesture 确确实实是独立于 touch event sequence 的事件,但是会干扰 event, Gesture 会先接管 touch event 的流程,然传递给 target-action 处理。
  6. Tap 事件和 view 是一一绑定的关系,多个 view 绑定相同 tap,最后一个有效。
  7. UIControl 的 target-event 只是对 touch event sequence 的采样后触发的逻辑,不会干扰手势也不会干扰 event 本身。注意 touch 的采样不会采样父元素的 touch event sequence。
  8. 在 UIResponder 的事件链路里 touchesBegan 等调用父类的作用不仅仅是向父类传播事件,也在于完成本次 event sequence 事件,到结束或取消。
  9. cancelTrackingWithEvent 是由 touchesCancelled:withEvent: 调用的。
  10. When adding an action method to a control, you specify both the action method and an object that defines that method to the addTarget:action:forControlEvents: method. If you specify nil for the target object, the control searches the responder chain for an object that defines the specified action method. 他的含义在于如果 level_2 元素上的某个类型的 action 对应的 target = nil,则回去找 level_1 的 类型,查询 respondsToSelector 是否为 YES。
    而不是理解为:level_2 不响应 event,然后让 level_1 来处理,这样理解是错误的。当然大部分情况下 target = nil ,还不如将这控件设置为 disabled(不考虑样式上的区别)。

原理

通过简单的调用栈比较发现 当 [UIWindow sendEvent:] 之后

  1. Touch 事件由,[UIWindow _sendTouchesForEvent] 分发
  2. TapGesture 事件由,[UIGestureEvnvironment _updateForEvent:window:] 里的 _wasDeliveredToGestureRecognizers 分发
    可见两者是两套系统。下面是来自苹果的文档里的一段,描述二者的关系。

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.
见文

实际应用距举例

1.模仿 AOP 拦截

在所有可用响应的事件的元素,执行自身逻辑之前拦截,做功能逻辑的鉴权。
典型的场景:
界面有“导出图片”,“转为 gif”,“上传到服务”三个按钮,这三个功能都需要用户购买 vip 服务。

  1. 传统的思路是在这三个方法的事件里,都添加前置判断逻辑,来判断是否是 vip ,不是则弹出购买 vip 的逻辑。这样的代码需要和真实的逻辑混在一起,即使是一个 if isVip() return这样的逻辑也是职责不清。
  2. 使用 TapGesture 独占的特性,我们在所有的按钮上都添加 cancelsTouchesInView = YES 的手势事件,单独处理 vip 判断逻辑。这样就做到了职责分离,即使后期,vip 服务和具体功能没有关联,也不需要改动“导出图片”等按钮的真实逻辑

2.[想象中的需求] App 内点击劫持

如果在 wechat 之类的第三方 App 上想做些事情,可以考虑使用 TapGesture 独占的特性来实现点击劫持。

3.无干扰的事件监听

我们知道 target-action 可以添加多个,因为它观察者的身份不会对旧的逻辑产生影响(当然同时我们也知道一个事实:对于 tapGesture 事件无能为力)
所以我们可以为所有 button 都添加一个自己的 action,用来监听用户对这个 button 的点击情况

  1. 当 button 没有添加业务事件时,代码逻辑里应该将这个 button disable(否则用户会丢失点击事件),这样我们的监听也不会触发
  2. 当 button 有业务事件时,点击后业务逻辑和监听逻辑都会执行
    well done。目前这种方式,被我使用在 apm SDK,SauronEye 里。用来代替传统的去 hook button 的 addTarget 方法的方式,更优雅

参考链接

  1. iOS事件处理,看我就够了~ ,本文没有考虑 cancelsTouchesInView 的影响
  2. iOS 触摸事件响应链
  3. Documentation of UIGestureRecognizer
  4. Documentation UIKit Views and Controls UIControl
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348