iOS 中的事件傳遞和響應機制 - 實踐篇

一個絕望的氣純發表於2019-01-19

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

需求場景示意圖

示意圖說明:白色 view 是藍色 view 的父檢視;藍色 view 是橙色 view 的父檢視。

  1. 需求一:點選重疊區,只有藍色 view(既父檢視)響應事件。

    一個最簡單的辦法是將子檢視的 isUserInteractionEnabled 設定為 false ;也可以在子檢視的 hitTest(_:with:) 方法裡面返回 nilsuperview ,可以達到同樣的效果。


  2. 需求二:點選螢幕上的任意地方;只有藍色 view 響應事件。

    一個最簡單的辦法是在藍色 view 的 hitTest(_:with:) 方法裡返回 self 。當事件傳遞到藍色 view 時,返回自己做為最適合觸發事件的控制元件。


  3. 需求三:點選橙色 view 的任意地方,藍色 view(既父檢視)響應事件。

    難點在於點選非重疊區時,藍色 view 不能接收到事件。為什麼會出現這種情況呢?回顧一下 “原理篇 - 如何尋找最適合的控制元件來處理事件” 就會發現,一個控制元件想要接收事件需要滿足兩個條件:

    1. 判斷自己能否觸發事件;
    2. 判斷觸控點是否在自己身上point(inside:with:) )。

    根據第二點,我們在點選非重疊區時,觸控點不在自己(藍色 view)身上,因此不能夠接收事件。

    再回顧一下這一節的要點:觸控事件傳遞的過程是從父控制元件傳遞到子控制元件的,如果父控制元件也不能接收事件,那麼子控制元件就不可能接收事件。

    那應該怎麼做呢?關鍵還是在第二點上(判斷觸控點是否在自己身上),這個方法返回的是一個 Bool 型別的值,換句話說,無論點是否在自己身上,只要讓這個方法返回 true,就可以讓藍色 view 接收事件。

    /// BlueView.swift
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // 首先正常返回,
        // 如果點不在自己身上,則判斷點是否在橙色 view 身上。
        // 注:此時的 subviews.first 代表橙色 view。
        return super.point(inside: point, with: event) || subviews.first!.frame.contains(point)
    }
    複製程式碼

    這樣做是可以的,也最簡單。但有一個問題,那就是如果橙色 view 也實現了 touches(_:with:) ,這時候是橙色 view 觸發事件而不是藍色 view。為什麼呢?

    因為只要判斷符合了條件,事件就會傳遞到橙色 view,而觸控點正好在橙色 view 身上,因此是橙色 view 觸發了事件。

    不過一般來說,有這種需求的子控制元件(橙色 view)都不會自己實現事件而是交給父控制元件(藍色 view)去處理。所以如果不想考慮這麼多的話,可以直接用上面的方法。但是如果想遮蔽掉子控制元件事件的觸發的話,還是有辦法解決的。

    解決的辦法就是攔截橙色 view 接收事件,只要在 BlueView.swift 中重寫 hitTest(_:with) 方法,返回指定的 view 來做為最適合處理事件的控制元件就可以了。

    /// BlueView.swift
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let hitView = super.hitTest(point, with: event)
        // 如果點在橙色 view 的身上,返回自己(藍色 view),不在則正常返回。
        // 注:此時的 subviews.first 代表橙色 view。
        return subviews.first!.frame.contains(point) ? self : hitView
    }
    複製程式碼

    這樣一來,事件就不會傳遞到橙色 view 了,只要點在橙色 view 身上,我就返回它的父檢視(藍色 view);如果不在,就正常返回(點選了藍色 view 還是藍色 view 觸發事件;點選了白色 view 則觸控點不在藍色 view 身上,此時白色 view 接收事件。)


  4. 需求四:點選重疊區時,橙色 view 和藍色 view 都響應事件。

    一個最簡單的辦法是在我們重新實現橙色 view 的 touches(_:with:) 方法後,呼叫 super.touches(_:with:) 讓它繼續將事件傳遞給下一個響應者(藍色 view)接收並處理事件。

    /// OrangeView.swift
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("Orange: \(#function)")
        // 繼續將事件傳遞給下一個響應者 (此時是藍色 view)
        super.touchesBegan(touches, with: event)
    }
    
    /// BlueView.swift
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("Blue", #function)
    }
    複製程式碼

  5. 需求五:正常響應,點選橙色 view 是橙色 view 響應事件;而點選藍色 view 是藍色 view 響應事件。

    可以說是經常出現的需求了,有時候我們需要處理超出父檢視區域的子檢視事件,但是點選超出區域的部分卻不能響應事件。那要怎麼做呢?

    其實這個問題在需求三的第一個示例中已經解決了,這裡不再贅述。

相關文章