注:根據史上最詳細的iOS之事件的傳遞和響應機制-原理篇從新整理(適當刪減及補充)。swift
在 iOS 中,只有繼承了 UIReponder
(響應者)類的對象才能接收並處理事件。其公共子類包括 UIView
、UIViewController
和 UIApplication
。ide
UIReponder
類中提供瞭如下 4 個對象方法來處理觸摸事件:spa
/// 觸摸開始
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸移動
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸取消(在觸摸結束以前)
/// 某個系統事件(例如電話呼入)會打斷觸摸過程
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸結束
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}
複製代碼
注意:code
若是手指同時觸摸屏幕,
touches(_:with:)
方法只會調用一次,Set<UITouch>
包含兩個對象;cdn若是手指先後觸摸屏幕,
touches(_:with:)
會依次調用,且每次調用時Set<UITouch>
只有一個對象。對象
UIApplication
管理的事件隊列中;UIApplication
會從事件隊列中取出最前面的事件,將之分發出去以便處理,一般,先發送事件給應用程序的主窗口( keyWindow
);touches(_:with:)
方法;touches(_:with:)
的默認實現是將事件順着響應者鏈(後面會說)一直傳遞下去,直到連 UIApplication
對象也不能響應事件,則將其丟棄。當事件觸發後,系統會調用控件的 hitTest(_:with:)
方法來遍歷視圖的層次結構,以肯定哪一個子視圖應該接收觸摸事件,過程以下:blog
hitTest(_:with:)
方法;point(inside:with:)
來判斷觸摸點是否在本身身上;subviews
,並重復前面三個步驟。直到找到包含觸摸點的最上層視圖,並返回這個視圖,那麼該視圖就是那個最適合的處理事件的 view;通俗一點來解釋就是,其實系統也沒法決定應該讓哪一個視圖處理事件,那麼就用遍歷的方式,依次找到包含觸摸點所在的最上層視圖,則認爲該視圖最適合處理事件。繼承
注意:隊列
觸摸事件傳遞的過程是從父控件傳遞到子控件的,若是父控件也不能接收事件,那麼子控件就不可能接收事件。事件
hitTest(_:with:)
的調用時機
hitTest(_:with:)
方法(無論這個控件可否處理事件或觸摸點是否本身身上)。hitTest(_:with:)
的做用
返回一個最適合的 view 來處理觸摸事件。
注意:
若是
hitTest(_:with:)
方法中返回nil
,那麼該控件自己和其subview
都不是最適合的 view,而是該控件的父控件。在默認的實現中,若是肯定最終父控件是最適合的 view,那麼仍然會調用其子控件的
hitTest(_:with:)
方法(否則怎麼知道有沒有更適合的 view?參考 如何尋找最適合的控件來處理事件。)
hitTest(_:with:)
的默認實現override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 1. 判斷本身可否觸發事件
if !self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01 {
return nil
}
// 2.判斷觸摸點是否在本身身上
if !self.point(inside: point, with: event) {
return nil
}
// 3. 倒序遍歷 `subviews` ,並重復前面兩個步驟;
// 直到找到包含觸摸點的最前面的視圖,並返回這個視圖,那麼該視圖就是那個最合適的接收事件的 view;
for view in subviews.reversed() {
// 把座標轉換成控件上的座標
let p = self.convert(point, to: view)
if let hitView = view.hitTest(p, with: event) {
return hitView
}
}
return self
}
複製代碼
找到最適合的 view 接收事件後,若是不重寫實現該 view 的 touches(_:with:)
方法,那麼這些方法的默認實現是將事件順着響應者鏈向下傳遞, 將事件交給下一個響應者去處理。
能夠說,響應者鏈是由多個響應者對象連接起來的鏈條。UIReponder
的一個對象屬性 next
可以很好的解釋這一規則。
返回響應者鏈中的下一個響應者,若是沒有下一個響應者,則返回 nil
。
例如,UIView
調用此屬性會返回管理它的 UIViewController
對象(若是有),沒有則返回它的 superview
;UIViewController
調用此屬性會返回其視圖的 superview
;UIWindow
返回應用程序對象;共享的 UIApplication
對象則一般返回 nil
。
例如,咱們能夠經過 UIView
的 next
屬性找到它所在的控制器:
extension UIView {
var next = self.next
while next != nil { // 符合條件就一直循環
if let viewController = next as? UIViewController {
return viewController
}
// UIView 的下一個響應控件,直到找到控制器。
next = next?.next
}
return nil
}
複製代碼