當(dāng)我們點(diǎn)擊一個 button 時,button 的響應(yīng)消息機(jī)制分為兩塊:
首先在視圖層次中找到能響應(yīng)消息的那個視圖即 button;
然后在找到的視圖 button 中進(jìn)行事件處理;
UIButton 繼承關(guān)系:
UIButton < UIControl < UIView < UIResponder
UIButton 之所以能夠處理事件,是因為它繼承自 UIResponder。也就是說只有繼承自UIResponder的類才能處理事件。
找響應(yīng)者
如圖,找響應(yīng)者是從父 View 到子 View 過程查找。主要用到了 UIView 的hitTest:withEvent: 以及 pointInside:withEvent: 兩個方法。

原理如下:
當(dāng)用戶點(diǎn)擊屏幕時,會產(chǎn)生一個觸摸事件,系統(tǒng)會將該事件加入到一個由 UIApplication 管理的事件隊列中;
UIApplication 會從事件隊列中取出最前面的事件進(jìn)行分發(fā)以便處理,通常,先發(fā)送事件給應(yīng)用程序的主窗口(UIWindow);
主窗口會調(diào)用
hitTest:withEvent:方法在視圖(UIView)層次結(jié)構(gòu)中找到一個最合適的 UIView 來處理觸摸事件
(hitTest:withEvent:其實是 UIView 的一個方法,UIWindow 繼承自 UIView,因此主窗口 UIWindow 也是屬于視圖的一種);
hitTest:withEvent: 方法處理機(jī)制:
當(dāng)前 view 調(diào)用自身的 pointInside: withEvent:方法判斷觸摸點(diǎn)是否在自己范圍內(nèi):
若
pointInside: withEvent:方法返回 NO,則說明觸摸點(diǎn)不在自己范圍內(nèi),則當(dāng)前 view 的hitTest: withEvent:方法返回 nil,當(dāng)前 view上 的所有 subview 都不做判斷。有點(diǎn)領(lǐng)導(dǎo)的意見一票否決的味道。若
pointInside: withEvent:方法返回 YES,則說明觸摸點(diǎn)在自己的范圍內(nèi)。但無法判斷是否在自己身上還是在 subview 的身上。此時,遍歷所有的 subviews,對每個 subview 調(diào)用 hitTest 方法。這里要注意,遍歷的順序是從當(dāng)前 view 的 subviews 數(shù)組的尾部開始遍歷。因此離用戶最近的上層的 subview 會優(yōu)先被調(diào)用 hitTest 方法。一旦 hitTest 方法返回非空的 view,則被返回的 view 就是最終相應(yīng)觸摸事件的 view,尋找 hitTesting view 的階段到此結(jié)束,不再遍歷。
若當(dāng)前 view 的所有 subviews 的 hitTest 方法都返回 nil,則當(dāng)前 view 的 hitTest 方法返回 self 作為最終的 hitTesting view,處理結(jié)束。
舉個例子,更加清晰的了解下:
如圖:

當(dāng)用戶點(diǎn)擊ViewD所在的區(qū)域時會進(jìn)行以下hit-Testing:
ViewA 的 pointInside 返回 YES,因為觸摸點(diǎn)在其 bounds 內(nèi)。遍歷 ViewA 的兩個 subview;
ViewB 的 pointInside 返回 NO,因為觸摸點(diǎn)不在其 bounds 內(nèi),ViewB 的 hitTest 方法返回 nil。而且發(fā)生一票否決,在 ViewB 上的所有 subviews 受到牽連將不再進(jìn)行 hit-Testing 處理。
ViewC 的 pointInside 返回 YES,因為觸摸點(diǎn)在其 bounds 范圍內(nèi),ViewC 的 hitTest 方法返回默認(rèn)處理,也就是
return [super hitTest:point withEvent:event];遍歷 ViewC 的兩個 subview;ViewD的 pointInside 返回 YES,因為觸摸點(diǎn)在其 bounds 范圍內(nèi),且ViewD 沒有 subview,因此 hitTest 方法返回其自己。hitTesting view 找到,結(jié)束處理
需要注意的地方:
1、hitTest 方法調(diào)用 pointInside 方法;
2、hit-Testing 過程是從 superView 向 subView 逐級傳遞,也就是從層次樹的根節(jié)點(diǎn)向葉子節(jié)點(diǎn)傳遞;
3、遇到以下設(shè)置時,view 的 pointInside 將返回NO,hitTest 方法返回 nil:
- view.isHidden=YES;
- view.alpah<=0.01;
- view.userInterfaceEnable=NO;
- control.enable=NO;(UIControl的屬性)
事件響應(yīng)
在上一部分已經(jīng)找到了響應(yīng)者,這個響應(yīng)者就會執(zhí)行相應(yīng)的 touch 系列方法,系統(tǒng)默認(rèn)處理事件之后將不繼續(xù)向下一響應(yīng)者傳遞。我們自己可以根據(jù)需要,通過復(fù)寫方法把當(dāng)前事件向下一響應(yīng)者進(jìn)行傳遞。
可以復(fù)寫下列方法對事件進(jìn)行處理
//觸摸開始,手指觸碰屏幕
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//觸摸結(jié)束,手指離開屏幕
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//觸摸取消(如電話接入的時候)
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//手指移動(會調(diào)用多次)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//3D touch 9.1之后加入的3D觸摸事件
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches
舉個例子:
新建一個 Single View App,在 ViewController.m 中:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//加上這一句,事件就可以向下一個響應(yīng)者傳遞
[super touchesBegan:touches withEvent:event];
NSLog(@"viewController touch begin");
}
在 AppDelegate.m 中:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"appDelegate touch begin");
}
這樣我們就實現(xiàn)了將事件向下一個響應(yīng)者傳遞。
響應(yīng)者鏈
下圖響應(yīng)者鏈鏈來自官網(wǎng)

我們也可以通過代碼打印響應(yīng)者鏈:
- (IBAction)click:(id)sender {
UIResponder *res = sender;
while (res) {
NSLog(@"*************************************\n%@",res);
res = [res nextResponder];
}
}
- UIView 的 nextResponder 屬性,如果有管理此 view 的 UIViewController 對象,則為此 UIViewController 對象;否則 nextResponder 即為其 superview。
- UIViewController 的 nextResponder 屬性為其管理 view 的 superview.
- UIWindow 的 nextResponder 屬性為 UIApplication 對象。
- UIApplication 的 nextResponder 屬性為 nil。
解釋一下:
1、如果 hit-test view 或 first responder 不處理此事件,則將事件傳遞給其 nextResponder 處理,若有 UIViewController 對象則傳遞給 UIViewController,傳遞給其 superView。
2、如果 view 的 viewController 也不處理事件,則 viewController 將事件傳遞給其管理 view 的 superView。
3、視圖層級結(jié)構(gòu)的頂級為 UIWindow 對象,如果 window 仍不處理此事件,傳遞給 UIApplication.
4、若 UIApplication 對象不處理此事件,則事件被丟棄。
了解響應(yīng)者鏈有時候可以幫我解決一些實際問題。我舉個例子,我們知道,當(dāng)提供給你一個ViewController你可以很容易得到它的view,一句代碼的事情:
viewWanted = someViewController.view;
但如果反過來呢?當(dāng)給你一個view,讓你找到其所在的ViewController呢?這時候響應(yīng)者鏈可以幫上忙了,代碼如下:
@implementation UIView (FindController)
-(UIViewController*)parentController{
UIResponder *responder = [self nextResponder];
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController*)responder;
}
responder = [responder nextResponder];
}
return nil;
}
@end
放一張完整的圖來理解下iOS觸摸事件的流動:

參考資料:
https://www.cnblogs.com/Quains/p/3369132.html