響應(yīng)鏈
iOS 大多數(shù)的事件分發(fā)都是依賴 UIResponder 響應(yīng)鏈來完成,響應(yīng)鏈?zhǔn)怯梢幌盗墟溄釉谝黄鸬捻憫?yīng)者組成的。一般情況下,一條響應(yīng)鏈開始于第一響應(yīng)者,結(jié)束于application對象。如果一個響應(yīng)者不能處理事件,則會將事件沿著響應(yīng)鏈傳到下一響應(yīng)者。那么當(dāng)我點擊屏幕,系統(tǒng)發(fā)生了什么呢?
我們以如下的視圖層級為例:
hit-test
在點擊之后,系統(tǒng)會優(yōu)先找出響應(yīng)的視圖,此過程叫做 hit-test
關(guān)于 hit-test 有 2 個方法與其有關(guān)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
當(dāng)你點擊到 View E 的時候系統(tǒng)做發(fā)生一些什么呢,
- 系統(tǒng)底層發(fā)出一個 event
- 系統(tǒng)分發(fā)事件到我們 App 的 UIApplication
- UIApplication 會對當(dāng)前的 KeyWindow 調(diào)用 hitTest 方法來確定觸摸的位置時候在 Window 內(nèi),hitTest 方法會調(diào)用 pointInSide 方法來確認一個點是否包含在我們 Window 視圖之類。
- UIWindow 會對其子視圖,也就是 View A 調(diào)用 hitTest ,hitTest 如步驟 3,調(diào)用 pointInSide 來確定點時候愛自身之類,這個時候返回 NO, 因為 E 不在 A 的里面。
- ViewC 對其 自身做 hitTest 和 pointInSide ,返回 YES, 因為 E 在 C 內(nèi)。
- View D 對其 自身做 hitTest 和 pointInSide ,返回 NO, 因為 E 不在 D 內(nèi)。
- View E 對其 自身做 hitTest 和 pointInSide ,返回 YES
- 由于 View E 是 hitTest 的最終點,沒有其他子視圖,于是結(jié)束 hitTest 的流程,返回自身,結(jié)束遞歸回調(diào)。
像上面的步驟,我們將尋找的過程稱之為 hit-Testing, 這個被找到的 View 稱之為 hit-Test View 。
找到 hit-Test View 之后我們便可以確定響應(yīng)的對象,
touchesEvent
確定了第一響應(yīng)者之后, touchesBegan,touchesMoved,touchesEnded 方法會根據(jù)情況依次被調(diào)用。
關(guān)于 touch 的方法有 4 個分別是:
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
事件傳遞
最有機會處理事件的對象是hit-test視圖或第一響應(yīng)者。如果這兩者都不能處理事件,UIKit就會將事件傳遞到響應(yīng)鏈中的下一個響應(yīng)者。每一個響應(yīng)者確定其是否要處理事件或者是通過nextResponder方法將其傳遞給下一個響應(yīng)者。這一過程一直持續(xù)到找到能處理事件的響應(yīng)者對象或者最終沒有找到響應(yīng)者。

當(dāng)系統(tǒng)檢測到一個事件時,將其傳遞給初始對象,這個對象通常是一個視圖。然后,會按以下路徑來處理事件(我們以左圖為例):
初始視圖(initial view)嘗試處理事件。如果它不能處理事件,則將事件傳遞給其父視圖。
初始視圖的父視圖(superview)嘗試處理事件。如果這個父視圖還不能處理事件,則繼續(xù)將視圖傳遞給上層視圖。
上層視圖(topmost view)會嘗試處理事件。如果這個上層視圖還是不能處理事件,則將事件傳遞給視圖所在的視圖控制器。
視圖控制器會嘗試處理事件。如果這個視圖控制器不能處理事件,則將事件傳遞給窗口(window)對象。
窗口(window)對象嘗試處理事件。如果不能處理,則將事件傳遞給單例app對象。
如果app對象不能處理事件,則丟棄這個事件。
從上面可以看到,視圖、視圖控制器、窗口對象和app對象都能處理事件。
手勢識別器對響應(yīng)鏈的影響

手勢相對于 UIView 的響應(yīng)鏈擁有更高的級別,在事件分發(fā)的時候會優(yōu)先給手勢處理,是否進行正常的響應(yīng)鏈流程則有手勢識別器來決定。
cancelsTouchesInView
此屬性可以中斷響應(yīng)鏈的流程,響應(yīng)流程會取消,使 touchesEnded
中斷,而 touchesCancelled
被調(diào)用。
delaysTouchesBegan
此屬性可以延遲響應(yīng)鏈的開始的流程,使 touchesBegan
延后導(dǎo)致響應(yīng)鏈被取消。
delaysTouchesEnded
此屬性可以延遲響應(yīng)鏈的開始的末尾,使 touchesEnded
延后,從而響應(yīng)鏈不能完整的結(jié)束。
手勢的代理
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
手勢識別以及后續(xù)事件是否開始激活,返回 NO 則終止了手勢。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
這個方法指示出 gestureRecognizer 是否可以和其他的 gestureRecognizer 一起處理事件。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
返回 YES 時,當(dāng) gestureRecognizer 和 otherGestureRecognizer 同時響應(yīng)時候,gestureRecognizer 失效
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
返回 YES 時,當(dāng) gestureRecognizer 和 otherGestureRecognizer 同時響應(yīng)時候,otherGestureRecognizer 失效
UIControl 及其子類的特殊化
上文提到 手勢識別器可以攔截一個響應(yīng)鏈,那么我的 UIButton 為什么可以正常工作呢?
文檔中有注明
When a control-specific event occurs, the control calls any associated action methods right away. Action methods are dispatched through the current UIApplication object, which finds an appropriate object to handle the message, following the responder chain if needed.
一個 UIControl 及其子類 、方法,其響應(yīng)事件的方式不同于普通的UIView,它們的事件處理由UIApplication直接分發(fā),和普通的UIView完全不同。所以自然手勢識別器無法影響。