注:根據(jù)史上最詳細的iOS之事件的傳遞和響應(yīng)機制-原理篇重新整理(適當(dāng)刪減及補充)。
在 iOS 中,只有繼承了 UIReponder
(響應(yīng)者)類的對象才能接收并處理事件畦戒。其公共子類包括 UIView
、UIViewController
和 UIApplication
。
UIReponder
類中提供了以下 4 個對象方法來處理觸摸事件:
/// 觸摸開始
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸移動
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸取消(在觸摸結(jié)束之前)
/// 某個系統(tǒng)事件(例如電話呼入)會打斷觸摸過程
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸結(jié)束
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}
注意:
如果手指同時觸摸屏幕棕兼,
touches(_:with:)
方法只會調(diào)用一次,Set<UITouch>
包含兩個對象抵乓;如果手指前后觸摸屏幕伴挚,
touches(_:with:)
會依次調(diào)用,且每次調(diào)用時Set<UITouch>
只有一個對象灾炭。
iOS 中的事件傳遞
事件傳遞和響應(yīng)的整個流程
- 觸發(fā)事件后茎芋,系統(tǒng)會將該事件加入到一個由
UIApplication
管理的事件隊列中; -
UIApplication
會從事件隊列中取出最前面的事件蜈出,將之分發(fā)出去以便處理田弥,通常,先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow
)铡原; - 主窗口會在視圖層次結(jié)構(gòu)中<u>找到一個最適合的視圖</u>來處理觸摸事件偷厦;
- 找到適合的視圖控件后商叹,就會調(diào)用該視圖控件的
touches(_:with:)
方法; -
touches(_:with:)
的默認實現(xiàn)是將事件順著響應(yīng)者鏈(后面會說)一直傳遞下去只泼,直到連UIApplication
對象也不能響應(yīng)事件剖笙,則將其丟棄。
如何尋找最適合的控件來處理事件
當(dāng)事件觸發(fā)后辜妓,系統(tǒng)會調(diào)用控件的 hitTest(_:with:)
方法來遍歷視圖的層次結(jié)構(gòu)枯途,以確定哪個子視圖應(yīng)該接收觸摸事件,過程如下:
- 調(diào)用自己的
hitTest(_:with:)
方法籍滴; - 判斷自己能否觸發(fā)事件酪夷、是否隱藏、alpha <= 0.01孽惰;
- 調(diào)用
point(inside:with:)
來判斷觸摸點是否在自己身上晚岭; - 倒序遍歷
subviews
,并重復(fù)前面三個步驟勋功。直到找到包含觸摸點的最上層視圖坦报,并返回這個視圖,那么該視圖就是那個最適合的處理事件的 view狂鞋; - 如果沒有符合條件的子控件片择,就認為自己最適合處理事件,也就是自己是最適合的 view骚揍;
通俗一點來解釋就是字管,其實系統(tǒng)也無法決定應(yīng)該讓哪個視圖處理事件,那么就用遍歷的方式信不,依次找到包含觸摸點所在的最上層視圖嘲叔,則認為該視圖最適合處理事件。
注意:
觸摸事件傳遞的過程是從父控件傳遞到子控件的抽活,如果父控件也不能接收事件硫戈,那么子控件就不可能接收事件。
尋找最適合的的 view 的底層剖析
-
hitTest(_:with:)
的調(diào)用時機- 事件開始產(chǎn)生時會調(diào)用下硕;
- 只要事件傳遞給一個控件丁逝,就會調(diào)用這個控件的
hitTest(_:with:)
方法(不管這個控件能否處理事件或觸摸點是否自己身上)。
-
hitTest(_:with:)
的作用返回一個最適合的 view 來處理觸摸事件卵牍。
注意:
如果
hitTest(_:with:)
方法中返回nil
果港,那么該控件本身和其subview
都不是最適合的 view,而是該控件的父控件糊昙。在默認的實現(xiàn)中,如果確定最終父控件是最適合的 view谢谦,那么仍然會調(diào)用其子控件的
hitTest(_:with:)
方法(不然怎么知道有沒有更適合的 view释牺?參考 如何尋找最適合的控件來處理事件萝衩。)
hitTest(_:with:)
的默認實現(xiàn)
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 1. 判斷自己能否觸發(fā)事件
if !self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01 {
return nil
}
// 2.判斷觸摸點是否在自己身上
if !self.point(inside: point, with: event) {
return nil
}
// 3. 倒序遍歷 `subviews` ,并重復(fù)前面兩個步驟没咙;
// 直到找到包含觸摸點的最前面的視圖猩谊,并返回這個視圖,那么該視圖就是那個最合適的接收事件的 view祭刚;
for view in subviews.reversed() {
// 把坐標(biāo)轉(zhuǎn)換成控件上的坐標(biāo)
let p = self.convert(point, to: view)
if let hitView = view.hitTest(p, with: event) {
return hitView
}
}
return self
}
iOS 中的事件響應(yīng)
找到最適合的 view 接收事件后牌捷,如果不重寫實現(xiàn)該 view 的 touches(_:with:)
方法,那么這些方法的默認實現(xiàn)是將事件順著響應(yīng)者鏈向下傳遞涡驮, 將事件交給下一個響應(yīng)者去處理暗甥。
可以說,響應(yīng)者鏈?zhǔn)怯啥鄠€響應(yīng)者對象鏈接起來的鏈條捉捅。UIReponder
的一個對象屬性 next
能夠很好的解釋這一規(guī)則撤防。
UIReponder().next
返回響應(yīng)者鏈中的下一個響應(yīng)者,如果沒有下一個響應(yīng)者棒口,則返回 nil
寄月。
例如,UIView
調(diào)用此屬性會返回管理它的 UIViewController
對象(如果有)无牵,沒有則返回它的 superview
漾肮;UIViewController
調(diào)用此屬性會返回其視圖的 superview
;UIWindow
返回應(yīng)用程序?qū)ο缶セ伲还蚕淼?UIApplication
對象則通常返回 nil
克懊。
例如,我們可以通過 UIView
的 next
屬性找到它所在的控制器:
extension UIView {
var next = self.next
while next != nil { // 符合條件就一直循環(huán)
if let viewController = next as? UIViewController {
return viewController
}
// UIView 的下一個響應(yīng)控件充岛,直到找到控制器保檐。
next = next?.next
}
return nil
}