應(yīng)用程序使用響應(yīng)者對(duì)象接收和處理事件。響應(yīng)者對(duì)象是UIResponder類的任何實(shí)例,常見(jiàn)的子類包括UIView、UIViewController和UIApplication。響應(yīng)者接收原始事件數(shù)據(jù),必須處理該事件或?qū)⑵滢D(zhuǎn)發(fā)給另一個(gè)響應(yīng)程序?qū)ο?。?dāng)應(yīng)用程序接收到事件時(shí),UIKit會(huì)自動(dòng)將該事件定向到最合適的響應(yīng)程序?qū)ο螅吹谝豁憫?yīng)者。
在iOS程序中響應(yīng)者對(duì)象的擺放是有前后關(guān)系的,多個(gè)響應(yīng)者對(duì)象有序的連接起來(lái)的鏈條就叫“響應(yīng)者鏈”。
創(chuàng)建一個(gè)UIView的子類ChainView,重寫(xiě)touchesBegan方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@ --- touchesBegan withEvent ---", self.name);
UIResponder * next = [self nextResponder];
NSMutableString * prefix = @"".mutableCopy;
while (next != nil) {
NSLog(@"%@%@", prefix, [next class]);
[prefix appendString: @"--"];
next = [next nextResponder];
}
}
創(chuàng)建一個(gè)ChainView實(shí)例添加到ViewController.view,點(diǎn)擊log輸出:
2020-09-24 11:08:56.494864+0800 001--ClientDemo[83858:1377593] greenView --- touchesBegan withEvent ---
ClientDemo[83858:1377593] UIView
ClientDemo[83858:1377593] --ViewController
ClientDemo[83858:1377593] ----UIWindow
ClientDemo[83858:1377593] ------UIApplication
ClientDemo[83858:1377593] --------AppDelegate
選擇不同版本iOS有不同輸出,在iOS12之前是上面輸出,iOS12及以后輸出如下
2020-09-24 11:11:41.146777+0800 001--ClientDemo[84116:1402680] greenView --- touchesBegan withEvent ---
ClientDemo[84116:1402680] UIView
ClientDemo[84116:1402680] --ViewController
ClientDemo[84116:1402680] ----UIDropShadowView
ClientDemo[84116:1402680] ------UITransitionView
ClientDemo[84116:1402680] --------UIWindow
ClientDemo[84116:1402680] ----------UIWindowScene
ClientDemo[84116:1402680] ------------UIApplication
ClientDemo[84116:1402680] --------------AppDelegate
這里及以下都以iOS11上為例。上面打印的是本例中事件響應(yīng)傳遞關(guān)系也即事件響應(yīng)者鏈,響應(yīng)者鏈?zhǔn)鞘录鬟f鏈的逆序,則事件傳遞鏈如下圖:

UIView 的 controller 是直接管理它的 UIViewController (也就是 VC.view.nextResponder = VC ),如果當(dāng)前 View 不是 ViewController 直接管理的 View,則 nextResponder 是它的 superView( view.nextResponder = view.superView )。
UIViewController 的 nextResponder 是它直接管理的 View 的 superView( VC.nextResponder = VC.view.superView )。
UIWindow 的 nextResponder 是 UIApplication 。
UIApplication 的 nextResponder 是 AppDelegate。
有了響應(yīng)鏈,并且找到了第一個(gè)響應(yīng)事件的對(duì)象,接下來(lái)就是把事件發(fā)送給這個(gè)響應(yīng)者了。 UIApplication中有個(gè)sendEvent:的方法,在UIWindow中同樣也可以發(fā)現(xiàn)一個(gè)同樣的方法。UIApplication是通過(guò)這個(gè)方法把事件發(fā)送給UIWindow,然后UIWindow通過(guò)同樣的API,把事件發(fā)送給hit-testview。
響應(yīng)鏈工作步驟
- 事件產(chǎn)生
- 事件順著傳遞鏈 AppDelegate---> UIApplication --->UIWindow ---> 查找第一響應(yīng)者,查找過(guò)程主要用到兩個(gè)函數(shù):
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- 找到最合適響應(yīng)者視圖后,視圖會(huì)調(diào)用自己的touches方法處理事件。如果自身沒(méi)有做處理,UIKit那么會(huì)逆著事件傳遞鏈,向上查找,直到找到能夠響應(yīng)這個(gè)事件的視圖。當(dāng)將事件傳遞給UIApplication對(duì)象時(shí),如果該對(duì)象是UIResponder實(shí)例,而不屬于響應(yīng)程序鏈的一部分,則可能會(huì)將事件傳遞給應(yīng)用程序委托AppDelegate。最終如果沒(méi)有對(duì)象響應(yīng)該事件,該事件會(huì)被丟棄(不是Crash?。?。
示例
左側(cè)是實(shí)現(xiàn)效果,其中設(shè)置了cyanView.userInteractionEnabled = NO,右側(cè)是當(dāng)前程序的響應(yīng)者鏈(同級(jí)視圖左邊線與右邊添加到父視圖上)。

修改上面提到的的ChainView實(shí)現(xiàn):
@interface ChainView : UIView
@property(nonatomic, copy) NSString *name;
@end
@implementation ChainView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@ --- touchesBegan withEvent ---", self.name);
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ --- hitTest withEvent", self.name);
UIView * view = [super hitTest:point withEvent:event];
NSLog(@"%@ --- hitTest withEvent --- hitTestView:%@", self.name, ((ChainView *)view).name);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL isInside = [super pointInside:point withEvent:event];
NSLog(@"%@ --- pointInside withEvent --- isInside:%d", self.name, isInside);
return isInside;
}
@end
點(diǎn)擊blueView和cyanView的重疊處,log輸出:
2020-09-23 16:48:39.439271+0800 TT[72680:1155021] redView --- hitTest withEvent
2020-09-23 16:48:39.439426+0800 TT[72680:1155021] redView --- pointInside withEvent
2020-09-23 16:48:39.439538+0800 TT[72680:1155021] redView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.439644+0800 TT[72680:1155021] yllowView --- hitTest withEvent
2020-09-23 16:48:39.439933+0800 TT[72680:1155021] yllowView --- pointInside withEvent
2020-09-23 16:48:39.440452+0800 TT[72680:1155021] yllowView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.440935+0800 TT[72680:1155021] purpleView --- hitTest withEvent
2020-09-23 16:48:39.441447+0800 TT[72680:1155021] purpleView --- pointInside withEvent
2020-09-23 16:48:39.442443+0800 TT[72680:1155021] purpleView --- pointInside withEvent --- isInside:0
2020-09-23 16:48:39.442865+0800 TT[72680:1155021] purpleView --- hitTest withEvent --- hitTestView:(null)
2020-09-23 16:48:39.443304+0800 TT[72680:1155021] orangeView --- hitTest withEvent
2020-09-23 16:48:39.443728+0800 TT[72680:1155021] orangeView --- pointInside withEvent
2020-09-23 16:48:39.444398+0800 TT[72680:1155021] orangeView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.444861+0800 TT[72680:1155021] cyanView --- hitTest withEvent
2020-09-23 16:48:39.445325+0800 TT[72680:1155021] cyanView --- hitTest withEvent --- hitTestView:(null)
2020-09-23 16:48:39.445743+0800 TT[72680:1155021] blueView --- hitTest withEvent
2020-09-23 16:48:39.446166+0800 TT[72680:1155021] blueView --- pointInside withEvent
2020-09-23 16:48:39.446590+0800 TT[72680:1155021] blueView --- pointInside withEvent --- isInside:1
2020-09-23 16:48:39.446985+0800 TT[72680:1155021] blueView --- hitTest withEvent --- hitTestView:blueView
2020-09-23 16:48:39.447281+0800 TT[72680:1155021] orangeView --- hitTest withEvent --- hitTestView:blueView
2020-09-23 16:48:39.447678+0800 TT[72680:1155021] yllowView --- hitTest withEvent --- hitTestView:blueView
2020-09-23 16:48:39.448096+0800 TT[72680:1155021] redView --- hitTest withEvent --- hitTestView:blueView
- 首先是redView的hitTest被調(diào)用,hitTest里通過(guò)pointInside判斷事件在redView相應(yīng)范圍內(nèi),向下判斷redView的子視圖;
- 子視圖的判斷是逆序的,所以先判斷yellowView,事件在yellowView響應(yīng)范圍,繼續(xù)判斷yellowView的子視圖;
- 依然是因?yàn)樽右晥D的判斷是逆序的,所以先判斷purpleView,沒(méi)有在purpleView響應(yīng)范圍;判斷orangeView通過(guò),繼續(xù)向下判斷orangeView的子視圖;
- 在判斷cyanView的時(shí)候,因?yàn)樵O(shè)置了cyanView.userInteractionEnabled = NO,所以cyanView不能響應(yīng)事件;
- 最后判斷blueView是第一響應(yīng)者。逆著響應(yīng)者鏈通知前面的響應(yīng)者 blueView是第一響應(yīng)者,blueView的touches事件調(diào)用。
事件攔截
改變第一響應(yīng)者

在需要攔截的 view 中重寫(xiě) hitTest 方法改變第一響應(yīng)者。
比如上面示例中,讓事件在yellowView中斷,由yellowView響應(yīng):
在ChainView中重新實(shí)現(xiàn)hitTest:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <=
0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
if([self.name isEqualToString:@"yellowView"]){
return self;
}
}
return nil;
}
限制第一響應(yīng)者范圍
下圖中的按鈕,只有中間圓形部分可以點(diǎn)擊,四個(gè)角上不響應(yīng)事件:

創(chuàng)建一個(gè)繼承自UIButton的CustomButton,實(shí)現(xiàn):
@implementation CustomButton
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
// 判斷觸摸位置是否在當(dāng)前視圖內(nèi)
if ([self pointInside:point withEvent:event]) {
//逆序遍歷當(dāng)前對(duì)象的子視圖
__block UIView *hitView = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//坐標(biāo)轉(zhuǎn)換系 使坐標(biāo)基于子視圖
CGPoint convertedPoint = [self convertPoint:point toView:obj];
//調(diào)用子視圖的 hitTest 方法
hitView = [obj hitTest:convertedPoint withEvent:event];
// 如果子視圖返回一個(gè)view 遍歷終止
if (hitView) {
*stop = YES;
}
}];
//如果子視圖返回一個(gè)view 返回這個(gè)view
if(hitView) {
return hitView;
}
//所有子視圖都返回 nil, 則返回自身.
return self;
}
return nil;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGFloat x1 = point.x;
CGFloat y1 = point.y;
CGFloat x2 = self.frame.size.width/2;
CGFloat y2 = self.frame.size.height/2;
double dist = sqrt((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2));
//在以當(dāng)前控件中心為圓心,直徑為控件寬度的圓內(nèi)
if(dist <= self.frame.size.width/2){
return YES;
}
else{
return NO;
}
// return [super pointInside:point withEvent:event];
}
具體使用和UIButton一樣,有興趣可以自己測(cè)試一下效果。
事件轉(zhuǎn)發(fā)
有時(shí)候還需要將事件轉(zhuǎn)發(fā)出去。讓本不能響應(yīng)事件的 view 響應(yīng)事件,最常用的場(chǎng)景就是讓子視圖超出父視圖的部分也能響應(yīng)事件

cyanView的右下區(qū)域超出了父視圖 orangeView 的區(qū)域,如果不作處理,那么點(diǎn)擊超出的區(qū)域是無(wú)法響應(yīng)事件的,因?yàn)槌鰠^(qū)域的坐標(biāo)不在orangeView的范圍內(nèi),當(dāng)執(zhí)行到orangeView的 pointInside 的時(shí)候就會(huì)返回 NO。
想要讓超出區(qū)域響應(yīng)事件,就需要重寫(xiě)父視圖的 pointInside 或 hitTest 方法讓 pointInside 返回 YES 或 讓hitTest 直接返回cyanView
重寫(xiě)hitTest
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ --- hitTest withEvent", self.name);
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
// 觸摸點(diǎn)在視圖范圍內(nèi) 則交由父類處理
if ([self pointInside:point withEvent:event]) {
return [super hitTest:point withEvent:event];
}
// 如果觸摸點(diǎn)不在范圍內(nèi) 而在子視圖范圍內(nèi)依舊返回子視圖
NSArray<UIView *> * superViews = self.subviews;
// 倒序 從最上面的一個(gè)視圖開(kāi)始查找
for (NSUInteger i = superViews.count; i > 0; i--) {
UIView * subview = superViews[i - 1];
// 轉(zhuǎn)換坐標(biāo)系 使坐標(biāo)基于子視圖
CGPoint newPoint = [self convertPoint:point toView:subview];
// 得到子視圖 hitTest 方法返回的值
UIView * view = [subview hitTest:newPoint withEvent:event];
// 如果子視圖返回一個(gè)view 就直接返回 不在繼續(xù)遍歷
if (view) {
return view;
}
}
return nil;
重寫(xiě) pointInside 方法原理相同, 重點(diǎn)注意轉(zhuǎn)換坐標(biāo)系,就算他們不是一條響應(yīng)鏈上,也可以通過(guò)重寫(xiě) hitTest 方法轉(zhuǎn)發(fā)事件。