触摸、事件和响应者那些事

这篇文章是我在阅读相关苹果官方文档后总结整理出来的一些平常可能不太注意到,但是又比较有用的知识点。如有错误,欢迎指出。

事件传递

事件本质

什么是事件?官方文档的解释是:

Events in iOS represent fingers touching views of an application or the user shaking the device. One or more fingers touch down on one or more views, perhaps move around, and then lift from the view or views. As this is happening, iPhone’s Multi-Touch system registers these touches as events and sends them to the currently active application for processing

当触摸事件发生时,系统会把触摸注册为一个事件(Event),传递给系统处理。一个完整的手势过程,是从第一根手指触碰到屏幕开始,到最后一根手指离开屏幕为止。

当然,手机的摇晃也要算是Event(它属于UIEventType里的motion),不过那是另一回事了,我们后边会再说。

屏幕上的每一个触摸点用UITouch来表示。在整个手势过程中,每一个UITouch对象会被系统持有,但是它的状态是可变的,分别要经历touchesBegan, touchesMovedtouchesEnded 三个状态。
当然,个别的时候,一个UITouch会经历第四个状态:touchesCanceled。一个事件被取消通常是由于一个外部事件(例如来电)的产生,让系统终止了本次touch事件。

UITouch

每一个UITouch对象表示一根手指对屏幕的触摸,包含位置、大小、移动状况以及触摸的力度(力度仅在支持3Dtouch或者Apple Pencil的设备上管用)。
UITouch类有以下我觉得比较重要的属性和方法:

  • locationInView: 表示触摸点在给定视图对应坐标系下的位置。如果view参数传nil,那么给出的是touch在window对于坐标系中的位置。
  • previousLocation(in:) 表示前一次该touch在给定视图中的方位。
  • view 表示touch对象被“传送”到的view。注意,这个view并不一定是touch对象本身所在的view(即,不一定是用户手指点中的view)。例如,当一个gestureRecognizer接收到一个触摸事件时,view为nil,因为没有view在接收这个触摸。
  • preciseLocation(in:) 这个表示一个touch在给定视图中的精确方位。注意,不要把返回的CGPoint用于hitTest。有的时候hitTest返回值显示touch在给定view中,但是preciseLocation方法返回的值却表明touch不在view中。
  • phase:表示UITouch对象的几个阶段。按照顺序依次变化:began, moved, stationary, ended/canceled

响应者链

UIResponder

UIResponder是一个抽象类,被苹果称为事件处理的“主心骨”。具体到事件发生时,继承自UIResponder的对象主要有两个方面的职责:

  1. 通过覆写四个关于touches的方法,拦截并处理事件(如果该对象需要响应事件的话)。
  2. 将事件顺着响应者链向上传递(如果该对象不需要响应该事件的话)。

另外,inputView也可以作为事件的响应(在这里我把它理解为“输入响应”)。例如,当我们点击一个textView,这个view会变成First Responder,并显示它的 inputView。关于inputView和firstResponder我们会另起一篇文章来详细描述它。

接下来我们看几个UIResponder当中重要的属性和方法:

  • nextResponder

顾名思义,它表示响应者链中的下一个响应者。值得注意的是,UIResponder本身并不存储或者预先设置任何值给nextResponder,该属性默认设置为nil。到底谁是nextResponder还需要继承自它的类自己来覆写。例如,一个View的nextResponder可能是它的superView(如果有的话),也可能是viewController(如果该view就是根视图)。一个ViewController的nextResponder可能是UIWindow(如果其根视图是这个window的root view的话),也可能是另一个viewControllerB(如果viewController嵌套在viewControllerB中显示的话)。UIWindow的nextResponder就是UIApplication。UIAPPlication的nextResponder就是appDelegate(当且仅当这个delegate是UIResponder的实例而非一个view,viewController,或者app object本身)。

  • isFirstResponder
    字面意思,“是否是第一响应者”。关于这个第一响应者,目前我暂时无法获得一个准确的定义,但基本可以肯定的是,此处的第一响应者和事件传递过程中寻找的“最合适的响应者”并非同一回事。因此,这部分暂时略过,等找到准确定义之后再发文说明。

  • canBecomeFirstResponder
    字面意思,表示一个对象是否能够成为第一响应者。UIKit会把某些事件,例如motion event,分发给“第一响应者”。默认返回No。

  • becomeFirstResponder
    让消息接收者成为第一响应者。这个方法相信咱们在开发过程中都快要写烂了——遇到textField,调用此方法,让系统弹出键盘。文档指出,对某个对象调用该方法后,并不能保证该对象一定能够成为firstResponder,因为,UIKit会首先对当前的firstResponder发送resignFirstResponder消息,然而后者可能会失败(例如自定义的对象重写了resignFirstResponder,通过return NO拒绝退出第一响应者状态)。
    如果当前firstResponder成功地resign了,UIKit还要调用当前对象canBecomeFirstResponder方法,而如上文所言,后者默认返回NO。
    再如果,canBecomeFirstResponder返回了YES——那么该对象将成为第一响应者。至此,所有发送给第一响应者的事件都被指派给这个对象,且系统将会试图展示该对象的inputView。

    • (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *) event
      默认情况下,不管你用了几根手指去点这个view,touches集合中仅包含一个UITouch 对象。如果你希望接收到多个手指的触控,记得调用view.isMultipleTouchEnabled=true。
      有两个注意点:
  1. 该方法的默认实现是将事件沿响应者链向上传递。因此,如果你想要覆写该方法,要确保调用了super的touchesBegan方法以传递任何你自身不处理的事件
  2. 如果你覆写该方法的时候没有调用super,那么你需要在你的自定义类中同时调用其他touches相关的方法,哪怕在这些方法中什么都不做。

事件拦截

UIEvent

  1. 它用于表示用户和APP的一个交互的对象。
  2. 永远不要retain一个UIEvent对象,或者是其内部的属性。如果的确需要retain某个UIEvent自带的属性,应对后者使用copy操作。
  3. 包含四个type: touches, motion, remote-Control, presses。motion是由UIKit触发的(要和Core Motion Framework的motion event区分开来。)remote-Control指的是用户通过外部配件(比如耳机、遥控器)对设备发出的操作指令。Press事件指的是用户通过游戏控制器、遥控器等的实体按键来和设备进行的交互行为。所有的这些可以通过UIEvent的type和subtype属性来加以判断。

接下来介绍一些重要的属性和方法:

func touches(for view: UIView) -> Set<UITouch>?

返回该事件中,属于指定view上的所有touch。

func touches(for window: UIWindow) -> Set<UITouch>?

和上面类似。

func touches(for gesture: UIGestureRecognizer) -> Set<UITouch>?

返回该手势识别器所接收到的所有UITouch对象。

func coalescedTouches(for touch: UITouch) -> [UITouch]?

这个方法是在iOS9之后提出的,它利用了一种叫做“触摸合并”的技术。由于系统对touch的采样在touchesMoved方法中进行,而后者的调用频率最高也才60次/秒(如果主线程有其他高耗时的操作,该方法的调用频率甚至更低),这样,就不可避免地会出现“漏点”的情况。而在新的iPad Pro2代上,界面刷新率达到了120Hz(使用Apple pencil时刷新率一度飙升至200Hz),因此,使用传统的touchesMoved必然会造成一个奇观:用户的手指在前面划线,画出来的线在后边追赶用户的手指……抑或是用户命名画了一条弧线,得到的却是一条“折线”……
基于此,苹果提出了触摸拟合技术,它可以让你获取到所有在两次touchesMoved调用之间的UITouch对象。
使用方法如下:
''override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)"
''{
'' if let coalescedTouches = event.coalescedTouchesForTouch(touch) {
'' print("coalescedTouches:", coalescedTouches.count)
''
'' for coalescedTouch in coalescedTouches
'' {
'' //Additional operations
'' }
''
'' }
'' }

`func predictedTouches(for touch: UITouch) -> [UITouch]?

同样是在iOS9以后,同样是为了减少延迟,苹果还推出了触摸预测技术,它根据先前触摸的点,使用一套非常精密的算法,来大致预测下一个被触摸的点所在的坐标。因此,开发者可以使用预测出来的点来提前做好UI更新的准备。
使用方法和func coalescedTouches(for touch: UITouch) -> [UITouch]?基本一样,在此不多赘述。

UITouch,UIEvent,UIResponder,UIGestureRecognizer的区别与联系

这一部分就算作是本文的小结了。我们来梳理一下四者的区别和联系:

  • UITouch:表示一根手指在屏幕上的触摸、移动,其生命周期从手指触摸屏幕时开始,到手指离开屏幕(或者被cancel)为止。
  • UIGestureRecognizer: 手势识别器。一个手势可能要一根or多根手指来完成,因此一个手势包含多个UITouch对象。
  • UIEvent:表示“事件”,有三个大类:触摸事件、动作事件、远程事件。一个触摸事件(touch event)包含了一个或多个与该事件有关的触摸对象,后者用UITouch对象来表示。
  • UIResponder:响应事件的一个“抽象类”,需要响应事件的类必须继承自它。多个响应者组成响应者链。
    另外,一个完整的触摸序列,是从第一根手指按下开始,到最后一根手指一开屏幕为止。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容

  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 54,578评论 51 596
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,861评论 4 26
  • 本文来自:http://ios.jobbole.com/84081/ 前言: 按照时间顺序,事件的生命周期是这样的...
    HackerOnce阅读 2,781评论 1 10
  • 在接下来的两章中,您将创建 TouchTracker,该应用程序中用户可以通过触摸屏幕来画画。 在本章中,您将创建...
    titvax阅读 627评论 0 0
  • 响应者对象 在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之...
    JonesCxy阅读 657评论 0 0