iOS 中的事件傳遞和響應機制 - 原理篇

注:根據史上最詳細的iOS之事件的傳遞和響應機制-原理篇重新整理(適當刪減及補充)。

在 iOS 中,只有繼承了 UIReponder(響應者)類的對象才能接收并處理事件。其公共子類包括 UIView 、UIViewControllerUIApplication

UIReponder 類中提供了以下 4 個對象方法來處理觸摸事件:

/// 觸摸開始
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸移動
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸取消(在觸摸結束之前)
/// 某個系統(tǒng)事件(例如電話呼入)會打斷觸摸過程
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}
/// 觸摸結束
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}

注意:

  • 如果手指同時觸摸屏幕,touches(_:with:) 方法只會調用一次,Set<UITouch> 包含兩個對象;

  • 如果手指前后觸摸屏幕,touches(_:with:) 會依次調用,且每次調用時 Set<UITouch> 只有一個對象。

iOS 中的事件傳遞

事件傳遞和響應的整個流程

  1. 觸發(fā)事件后,系統(tǒng)會將該事件加入到一個由 UIApplication 管理的事件隊列中;
  2. UIApplication 會從事件隊列中取出最前面的事件,將之分發(fā)出去以便處理,通常,先發(fā)送事件給應用程序的主窗口( keyWindow );
  3. 主窗口會在視圖層次結構中<u>找到一個最適合的視圖</u>來處理觸摸事件;
  4. 找到適合的視圖控件后,就會調用該視圖控件的 touches(_:with:) 方法;
  5. touches(_:with:) 的默認實現(xiàn)是將事件順著響應者鏈(后面會說)一直傳遞下去,直到連 UIApplication 對象也不能響應事件,則將其丟棄。

如何尋找最適合的控件來處理事件

當事件觸發(fā)后,系統(tǒng)會調用控件的 hitTest(_:with:) 方法來遍歷視圖的層次結構,以確定哪個子視圖應該接收觸摸事件,過程如下:

  1. 調用自己的 hitTest(_:with:) 方法;
  2. 判斷自己能否觸發(fā)事件、是否隱藏、alpha <= 0.01;
  3. 調用 point(inside:with:) 來判斷觸摸點是否在自己身上;
  4. 倒序遍歷 subviews ,并重復前面三個步驟。直到找到包含觸摸點的最上層視圖,并返回這個視圖,那么該視圖就是那個最適合的處理事件的 view;
  5. 如果沒有符合條件的子控件,就認為自己最適合處理事件,也就是自己是最適合的 view;

通俗一點來解釋就是,其實系統(tǒng)也無法決定應該讓哪個視圖處理事件,那么就用遍歷的方式,依次找到包含觸摸點所在的最上層視圖,則認為該視圖最適合處理事件。

注意:

觸摸事件傳遞的過程是從父控件傳遞到子控件的,如果父控件也不能接收事件,那么子控件就不可能接收事件。

尋找最適合的的 view 的底層剖析

  • hitTest(_:with:) 的調用時機

    • 事件開始產生時會調用;
    • 只要事件傳遞給一個控件,就會調用這個控件的 hitTest(_:with:) 方法(不管這個控件能否處理事件或觸摸點是否自己身上)。
  • hitTest(_:with:) 的作用

    返回一個最適合的 view 來處理觸摸事件。

注意:

如果 hitTest(_:with:) 方法中返回 nil ,那么該控件本身和其 subview 都不是最適合的 view,而是該控件的父控件。

在默認的實現(xiàn)中,如果確定最終父控件是最適合的 view,那么仍然會調用其子控件的 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` ,并重復前面兩個步驟;
    // 直到找到包含觸摸點的最前面的視圖,并返回這個視圖,那么該視圖就是那個最合適的接收事件的 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
}

iOS 中的事件響應

找到最適合的 view 接收事件后,如果不重寫實現(xiàn)該 view 的 touches(_:with:) 方法,那么這些方法的默認實現(xiàn)是將事件順著響應者鏈向下傳遞, 將事件交給下一個響應者去處理。

響應者鏈示意圖

可以說,響應者鏈是由多個響應者對象鏈接起來的鏈條。UIReponder 的一個對象屬性 next 能夠很好的解釋這一規(guī)則。

UIReponder().next

返回響應者鏈中的下一個響應者,如果沒有下一個響應者,則返回 nil 。

例如,UIView 調用此屬性會返回管理它的 UIViewController 對象(如果有),沒有則返回它的 superviewUIViewController 調用此屬性會返回其視圖的 superview;UIWindow 返回應用程序對象;共享的 UIApplication 對象則通常返回 nil 。

例如,我們可以通過 UIViewnext 屬性找到它所在的控制器:

extension UIView {
    var next = self.next
    while next != nil { // 符合條件就一直循環(huán)
        if let viewController = next as? UIViewController {
            return viewController
        }
        // UIView 的下一個響應控件,直到找到控制器。
        next = next?.next   
    }
    return nil
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容