iOS开发中的事件处理
理论非原创,是对网上资料的整理以及Demo验证
一. UIResponder
1.1 事件处理简介
iOS中的事件:
在用户使用app的过程中会产生各种各样的事件

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”;
我们熟悉的 UIApplication、 UIViewController 和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象,都能够接收并处理事件。需要注意的是Core Animation layers 不是responders
第一响应者是指定的第一个接收事件的对象,比如一个View对象是第一响应者.一个对象可以通过以下方法成为第一响应者:
- 重写
canBecomeFirstResponder
方法,返回yes - 接收到一个
becomeFirstResponder
的menssage,如果需要,也可以自己给自己发送message成为第一响应者.
注意:确保你的app指定一个对象成为第一响应者之前,这个对象图像已经被绘画出来.
举个例子:通常你在重写 viewDidAppear方法内,执行becomeFirstResponder
方法.但是如果在viewWillAppear:中 指定 第一响应者,你的对象的图像还没有被建立好,所以becomeFirstResponder
会返回NO
1.2 UIResponder
UIResponder是所有响应对象的基类,UIResponder内部提供了以下方法来处理事件:
触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
UIView的触摸事件处理
UIView是UIResponder的子类,可以实现下列4个方法处理不同的触摸事件
一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
提示:touches中存放的都是UITouch对象
1.3 UITouch:
当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象,一根手指对应一个UITouch对象
UITouch的作用:
保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象
UITouch的属性:
触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
UITouch的方法:
- (CGPoint)locationInView:(UIView *)view;
返回值表示触摸在view上的位置
这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)previousLocationInView:(UIView *)view;
该方法记录了前一个触摸点的位置
UITouch的phase有什么用?
触摸事件在屏幕上有一个周期,即触摸开始、触摸点移动、触摸结束,还有中途取消。而通过phase可以查看当前触摸事件在一个周期中所处的状态。phase是UITouchPhase类型的,这是一个枚举配型,包含了
UITouchPhaseBegan(触摸开始)
UITouchPhaseMoved(接触点移动)
UITouchPhaseStationary(接触点无移动)
UITouchPhaseEnded(触摸结束)
UITouchPhaseCancelled(触摸取消)
1.4 UIEvent
UIEvent:每产生一个事件,就会产生一个UIEvent对象
UIEvent:称为事件对象,记录事件产生的时刻和类型
常见属性
事件类型
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)
Touches和event参数:
一次完整的触摸过程,会经历3个状态:
触摸开始:- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
触摸移动:- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
触摸结束:- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
触摸取消(可能会经历):- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
4个触摸事件处理方法中,都有NSSet *touches和UIEvent *event两个参数
一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸
疑问:默认触摸方法NSSet里面只能获得一个UITouch对象,为什么?
UIView默认不支持多点触控。也就是说不支持多只手指同时触摸。
如何让视图接收多点触摸?
需要设置它的multipleTouchEnabled属性为YES,默认状态下这个属性值为NO,即视图默认不接收多点触摸。。
如何判断用户当前是双击还是单击?
根据UITouch的tapCount属性的值。tapCount表示短时间内轻击屏幕的次数。因此可以根据tapCount判断单击、双击或更多的轻击。
根据tapCount点击的次数来设置当前视图的背景色(双击改变背景颜色)
轻击操作很容易引起歧义,比如当用户点了一次之后,并不知道用户是想单击还是只是双击的一部分,或者点了两次之后并不知道用户是想双击还是继续点击。为了解决这个问题,一般可以使用“延迟调用”函数。
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
if(touch.tapCount != 2){ // 如果不是双击
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(setBackgroundColor:) object:[UIColor orangeColor]];
} else { // 延时1执行改变背景的方法
[self performSelector:@selector(setBackgroundColor:) withObject:[UIColor orangeColor] afterDelay:1.0];
}
}
提示:iPhone开发中,要避免使用双击事件!
NSObject类的cancelPreviousPerformRequestWithTarget:selector:object方法取消指定对象的方法调用。
官方对该方法解释:
Cancels perform requests previously registered with performSelector:withObject:afterDelay:.
All perform requests are canceled that have the same target as aTarget, argument as anArgument, and selector as aSelector.
如果是带参数,那取消时的参数也要一致,否则不能取消成功
细节
检测tapCount可以放在touchesBegan也可以touchesEnded,不过一般后者更准确,因为touchesEnded可以保证所有的手指都已经离开屏幕,这样就不会把轻击动作和按下拖动等动作混淆。
不管是一个手指还是多个手指,轻击操作都会使每个触摸对象的tapCount加1,因此可以直接调用touches的anyObject方法来获取任意一个触摸对象然后判断其tapCount的值即可。
二. 事件传递,响应者链条
简介:
发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件 队列中,UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
UIView不接受触摸事件的三种情况:
不接收用户交互
userInteractionEnabled = NO
隐藏
hidden = YES
透明
alpha = 0.0 ~ 0.01
提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的
2.1 事件传递的详细过程:
概念: iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。
UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。
**hitTest:withEvent:方法的处理流程如下: **
- 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
- 若返回NO,则hitTest:withEvent:返回nil;
- 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图(top)一直到到最底层视图(bottom),即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
- 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
-
如所有子视图都返回NO,则hitTest:withEvent:方法返回自身(self)。
1-1-0-1.png
假如用户点击蓝色3
view
下面结合上图介绍hit-test view的流程:
-
白1
是UIWindow的根视图,因此,UIWindwo对象会首相对白1
进行hit-test; -
显然用户点击的范围是在
白1
的范围内,因此, pointInside:withEvent:返回了YES,这时会继续检查白1
的子视图; -
这时候会有两个分支,
绿色2
和红色2
:点击的范围不再
绿2
内,因此绿2
分支的 pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;点击的范围在
红2
内,即红2
的 pointInside:withEvent:返回YES; -
这时候有
蓝3
和绿3
两个分支:点击的范围不再
绿3
内,因此绿3
的 pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;点击的范围在
蓝3
内,即蓝3
的 pointInside:withEvent:返回YES,而由于再对蓝3
的子视图进行hit-test时全部返回了nil(也可以理解由于蓝3
没有子视图),因此,蓝3
的 hitTest:withEvent:会将蓝3
返回,再往回回溯,就是红2
的 hitTest:withEvent:返回蓝3
--->>白1
的hitTest:withEvent:返回蓝3
。 -
蓝3
是包含touch的所有view中层级最小的,所以他就是hit-test view -
至此,本次点击事件的第一响应者(
蓝3
)就通过响应者链的事件分发逻辑成功的找到了。
这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。
本次事件的传递顺序: UIApplication --> UIWindow -->白色BASEView1 -->红色2 -->蓝色3
上面找到了事件的第一响应者,接下来就该沿着寻找第一响应者的相反顺序来处理这个事件,如果UIWindow单例和UIApplication都无法处理这一事件,则该事件会被丢弃。
说明:
- 如果最终 hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;
- hitTest:withEvent:方法将会忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。
- 触摸事件的传递是从父控件传递到子控件
- <font color=#FF0000>如果父控件不能接收触摸事件,那么子控件就不能接收到触摸事件</font>
- 如上图情况,如果想在点击
黄4
的时候,让蓝3
成为第一响应者,而让黄4
不响应,可以有以下几种方法:
5.1 设置黄4
隐藏(hidden=YES),禁止用户操作(userInteractionEnabled=YES),alpha级别小于0.01(alpha<0.01)
5.2 重写黄4
的pointInside:withEvent:方法, 返回NO
5.3 重写蓝3
的 hitTest:withEvent:方法 如果得到的view是黄4
,返回self
示例:
点击�绿色view:
UIApplication --> UIWindow -->白色BASEView -->绿色View

点击半透明绿色view:

UIApplication --> UIWindow -->白色BASEView -->红色 -->半透明绿色
点击黄色View:

UIApplication --> UIWindow -->白色BASEView -->红色 -->蓝色 -->黄色
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,但是这仅仅是整个事件处理过程的第一步 找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理
touchesBegan…
touchesMoved…
touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理
如何找到最合适的控件来处理事件?
- 自己是否能接收触摸事件?-->否-->事件传递到此结束
- 触摸点是否在自己身上?-->否-->事件传递到此结束
- 从后往前遍历子控件,重复前面两个步骤
- 如果没有符合条件的子控件,那么就自己最合适
2.2 定制hitTest:withEvent:方法
如果父视图需要对对哪个子视图可以响应触摸事件做特殊控制,则可以重写hitTest:withEvent:或pointInside:withEvent:方法。
这里有几个例子:

在此例子中button,scrollview同为topView的子视图,但scrollview覆盖在button之上,这样在在button上的触摸操作返回的hit-test view为scrollview,button无法响应,可以修改topView的hitTest:withEvent:方法如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *result = [super hitTest:point withEvent:event];
CGPoint buttonPoint = [_underButton convertPoint:point fromView:self];
if ([_underButton pointInside:buttonPoint withEvent:event]) {
return _underButton;
}
return result;
}
2.3 响应者链条
响应者:继承了UIResponder的对象就是响应者
响应者链条:
- 由多个响应者对象连接起来的链条叫做响应者链条
- 什么是上一个响应者?
2.1. 如果当前这个view是控制器的view,控制器就是上一个响应者
2.2. 如果当前这个view不是控制器的view,那么父控件就是上一个响应者 - 利用响应者链条可以让多个控件处理同一个 "触摸事件"
3.1. 在最后适合的控件里调用super的touchesBegan方法,这样就将事件传给上一个响应,上一个响应者也可以处理事件了
响应者链条,总的来说是:
- 它是一种事件处理机制,由多个响应者对象连接起来的链条,使得事件可以沿着这些对象进行传递。
- 如果一个响应者对象不能处理某个事件或动作消息,则将该事件消息重新发送给链中的上一个响应者。
- 消息沿着响应者链向上、向更高级别的对象传递,直到最终被处理。
事件的完整处理过程:
-
先将事件对象由上往下传递(由父控制传给子控件),找到最适合的控件来处理
-
调用最合适的控件的touches...方法
-
如果调用了[super touch…],就会将事件顺着响应都链往上传递,传递给上一个响应者
-
接着上一个响应者就会调用的touches...方法
-
如果没有找到最适合的控件来处理事件,则将事件传回来窗口,窗口不处理事件,将事件传给UIApplication
-
如果Applicatoin不能处理事件,则将其丢弃
响应链中事件的传递遵循特殊的派送轨道
一个初始对象为hit-test view 或者first responder,如果这个对象不处理事件,UIKit会将这个事件传递给响应链中的next responder,每一个responder决定是自己处理该事件,还是将事件传递给自己的next responder.这个过程一直持续,直到事件被处理或者没有更多的responder了.
当iOS 检查出一个事件并查到事件的第一响应者(具有代表性的是个view),响应者链队列开始
响应者链条示意图:

左边的app,事件遵循以下流程:
- initial view 尝试处理事件或信息.如果它不能处理这个事件,将事件传递给它的superview,因为它不是它的view controller里view层级的最高层.
- superview 尝试处理事件或信息.如果它不能处理这个事件,将事件传递给它的superview,因为它仍旧不是它的view controller里view层级的最高层.
- topmost view 是view controller的view层级的最高层,它尝试处理事件或信息.如果它不能处理这个事件,将事件传递给它的view controller.
- view controller 尝试处理事件或信息.如果它不能处理这个事件,将事件传递给window.
- 如果window object 不能处理事件,它直接把事件传递给singleton app object
- 如果app object 不能处理事件,它会丢弃这个事件
右边的app遵循略微不同的路线,但是所有的事件event传递遵循下边的启发法:
- A view 将他的事件在它的view controller的 view层级 向上传递,知道到达topmost view最高层view
- topmost view 把事件传递给它的 view controller
-
view controller 把事件传递给 topmost view的superview
重复1-3步骤,直到事件传递给root view controller - root view controller把事件传递给 window object
- window 把事件传递给 app object
网友评论