響應者鏈工作原理
應用程序使用Responder
對象接收和處理時間,響應者對象是UIResponder
類的任何實例,常見的子類包括UIView
,UIViewController
和UIApplication
.響應者收到原始事件數據,并且必須處理該事件或將其轉發給另一個響應者對象,當您的應程序收到一個事件時,UIKit
會自動將該事件指向最合適的響應者對象,稱為第一響應者,未處理的事件從響應者傳遞到活動響應器中的響應者,這是應用程序的響應者對象的動態配置,應用程序中沒有單個響應者鏈,UIKit
定義了將對象從一個響應者傳遞到下一個響應器的默認規則,但是您可以隨時通過覆蓋響應者對象中相對應的屬性來更改這些規則.下圖顯示了應用程序的默認響應者鏈,其界面包含Label
,textField
,button
和兩個背景視圖,如果label不處理事件,UIKit
會將事件發送到其superView
對象,然后是窗口的根視圖,從根視圖響應者鏈轉移到擁有該視圖的控制器,然后將事件傳遞給window,如果window
不處理,UIKit
將事件傳遞給UIApplication
對象,并且可能將該事件傳遞給AppDelegate
,
對于每種類型的事件,UIKit
指定一個第一響應者,并首先發送事件到這個對象,第一響應者根據事件的類型而有所不同.
- Touch events
第一響應者是發生觸摸的視圖 - Press events
第一響應者是有焦點的響應者 - Shake-motion events
第一響應者是(UIKit
)指定為第一個響應者的對象 - Remote-control events
第一響應者是(UIKit
)指定為第一個響應者的對象 - Editing menu messages
第一響應者是(UIKit
)指定為第一個響應者的對象
與加速器,陀螺儀和磁力計相關的運動事件不遵循響應者連,Core Motion
將這些事件直接傳遞給你指定的對象,(https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/iPhoneOSTechOverview/CoreServicesLayer/CoreServicesLayer.html#//apple_ref/doc/uid/TP40007898-CH10-SW27)
當手指觸摸到屏幕中,觸摸到某一個控件到響應這個事件分為兩步:事件的傳遞與分發,和事件的響應
事件的傳遞涉及到UIView
中的兩個方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判斷當前點擊事件是否存在最優響應者(First Responder)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
//判斷當前點擊是否在控件的Bounds之內
UIKit使用基于視圖的命中測試來確定觸摸事件發生的位置。 具體來說,UIKit將觸摸位置與視圖層次結構中的視圖對象的邊界進行比較。 UIView的hitTest(_:with :)方法行進視圖層次結構,尋找包含指定觸摸的最深的子視圖。 該視圖成為觸摸事件的第一響應者
注意如果觸摸位置在視圖的邊界之外,則hitTest(_:with :)方法會忽略該視圖及其所有子視圖。 因此,當視圖的clipsToBounds屬性為false時,即使它們包含觸摸,也不會返回該視圖邊界之外的子視圖
事件的傳遞其實就是在事件產生于分發之后如何尋找最優響應的一個過程
你可以修改響應者對象的next屬性來更改響應者鏈,當你修改了此屬性時下一個響應者就是你返回的對象,
許多UIKit類已經覆蓋此屬性并返回特定對象。
UIView
對象,如果這個控制器的View是根視圖,那么next responder 就是viewController,否則,就是這個view的父視圖-
UIViewController
對象,- 如果該controller的view是window的根視圖,那么next responder就是window
- 如果controller是由另一個controller presented的,那么next responder是the presenting Controller.
UIWindow
對象,next responder 就是UIApplication
對象UIApplication
對象,那么 next responder 就是 app delegate ,但前提是appDelegate
是UIResponder
的一個實例,而不是view,viewcontroller或者是app本身.
(以上來自官方文檔)
事件的傳遞過程
1.觸碰屏幕產生事件UIEvent
并存入UIApplication
中事件隊列中,并在整個視圖結構中自下而上進行分發,如下圖
2.UIWindow
接收到事件開始進行最優響應視圖查詢過程(逆序遍歷子視圖)
3.當到UiviewController
這一層時同樣對其視圖開始最優響應視圖查詢,該查詢會調用上述提到的兩個方法,采用逆序查詢也是為了優化查找速度,畢竟后添加的視圖易于命中
命中查找流程
1.調用hitTest
方法進行最優響應視圖查找
- hidden = YES;
- userInteractionEnabled = NO;
- alpha < 0.01
這三種情況hitTest方法會返回nil ,即當前視圖下無最有相應視圖,無法響應事件
2.hitTest方法內部調用pointInside方法對點擊進行是否在當前視圖bounds內進行判斷,如果超出bounds范圍,則hitTest返回nil,未超出范圍繼續步驟3
3.對當前視圖下的subviews進行逆序步驟1,2查詢最優響應者,如果hitTest返回視圖,則說明當前視圖有最優響應者,可能是self也可能是subview,
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha < 0.01 || !self.userInteractionEnabled || self.hidden) {
return nil;
}
if (![self pointInside:point withEvent:event]) {
return nil;
}
__block UIView *hitView = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
hitView = [subview hitTest:point withEvent:event];
if (hitView) {
*stop = YES;
}
}];
return hitView ? : self;
}
總結
- 事件分發與傳遞:自上而下(
UIApplication
-window
-.....) - 事件響應:自下而上(
view
-superView
-.........)
打印結果: