關於iOS Responder Chain 的一些理解

基本概念

  • 響應者:它是 UIResponder/UIView/UIViewController/UIApplication 的實例。它會接受事件,而且它必須處理事件或將事件傳遞給下一個響應者。UIKit 會自動決定哪一個對象爲最合適的響應者,即第一響應者。
  • 響應鏈:響應者傳遞事件的過程。

響應鏈的傳遞流程

以官方文檔的圖舉例:ios

若是 text field 沒有處理事件, UIKit 會將事件傳遞給它的父視圖 UIView 對象,依次傳遞到 UIViewController 的根視圖,若還不能處理,則傳遞給 UIWindow 。若是 window 不能處理事件,則將該事件再傳遞給 UIApplication。bash

如何決定哪個響應者包含 touch 事件

UIKit 使用基於 view 的 hit-testing 去決定 touch 事件發生的位置。在發生 touch 事件後,UIKit 會比較 touch 的位置所在的視圖是否在視圖層級中。hitTest(_:with:)會將包含 touch 事件的最上層的視圖變爲第一響應者。app

若是 touch 的位置超出視圖的邊界,那 hitTest(_:with:) 方法會忽略該視圖的全部子視圖。所以,當一個視圖的 clipsToBounds 爲 false 時,即便 touch 的位置未超出子視圖的邊界,子視圖也不會響應該事件。ide

下面解釋一下若是 touch 的位置超出視圖的邊界,那 hitTest(_:with:) 方法會忽略該視圖的全部子視圖:函數

若是咱們有上圖層級結構,blueView 是 greenView 的子視圖,grayView 是 blueView 的子視圖,三個視圖都添加 UITapGestureRecognizer 手勢,若是你點擊 grayView 中超出 blueView 的部分它是不會響應 grayView 的 action,由於該部分已經超出了 blueView 部分,因此 blueView 及其全部子視圖都會被響應鏈忽略。因此,你點擊 grayView 中超出 blueView 的部分,響應者是 greenView,會響應 greenView 的 action 。ui

當發生一個 touch 事件時,UIKit 會建立一個 UITouch 的對象,並將它與一個視圖聯繫起來。須要注意的是,當 touch 的位置或者其餘參數改變時, UIKit 會將 UITouch 對象的信息更新。可是 UITouch 對象的 view 屬性並不會變。即便 UITouch 對象的位置已經超出原始視圖的邊界,UITouch 對象的 view 屬性值也不會改變。當 touch 結束,UIKit 會釋放 UITouch 對象。spa

hitTest(_:with:)

該方法主要用來返回接受 touch 事件的視圖層中最上層的且包含當前 point 的子視圖。code

它經過 point(inside:with:) 函數來尋找包含 point 的子視圖,若是 point(inside:with:) 返回 true,則再往上一直找到最上層且包含 point 的子視圖。cdn

你能夠經過重寫它來對某些子視圖隱藏響應事件。當視圖的 hidden 爲 true 或 isUserInteractionEnabled 爲 false 或 alpha 的值小於 0.01 的時候,這些視圖將被該方法忽略。對象

如何修改響應鏈

你能夠經過重寫響應者的 next 屬性來修改響應鏈。當你修改以後,下一個響應者即你返回的那個對象。

下面是 UIKit 各種默認下一個響應者:

  • UIView:若 view 是 Controller 的根視圖,下一個響應者爲 Controller;若不是根視圖,則下一個響應者爲它的父視圖。
  • UIViewController:若是 Controller 是 window 的根視圖,下一個響應者是 window 對象;若是 Controller1 是被 Controller2 present 的,那下一個響應者即Controller2。
  • UIWindow:下一個響應者爲 UIApplication 對象、
  • UIApplication:當 app delegate 是 UIResponder 一個實例,而且不是一個 view 、 view controller 或者 app 它自己時。UIApplication 的下一個響應者爲 app delegate。

關於 UIControl

若是咱們在添加手勢的視圖上面添加一個帶有 action 的 button ,若是咱們點擊 button 會觸發 視圖的手勢事件 仍是 button 的 action 呢?

答案是 button 的 action 。看官方文檔咱們會看見這麼一句話手勢處理器不會影響 UIKit controls 處理事件的能力。

不單單是 UIButton ,其餘下面的對象也不會受手勢識別器的影響:

  • UISwitch, UIStepper, UISegmentedControl, UIPageControl, UISlider UISwitch

以上這些皆爲 UIControl 的子類。

實際應用

擴大UIButton的點擊範圍

  • 建立一個 UIButton 的子類,重寫 point(inside:with:) 方法
// 將按鈕的點擊範圍上下左右擴大10pt的範圍,注意範圍不要超出父視圖的邊界,超出範圍依舊無效。
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    let newFrame = CGRect(x: bounds.origin.x - 10, y: bounds.origin.y - 10, width: bounds.size.width + 20, height: bounds.size.height + 20)
    return newFrame.contains(point)
}
複製代碼
  • 設置 button 的 contentEdgeInsets 屬性
button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
複製代碼

解決 scrollView及子類TableView/CollectionView 右滑手勢和系統的右滑返回手勢的衝突

// 經過修改響應者依賴 來使 scrollView及子類 的手勢響應失效
if let rec = navigationController?.interactivePopGestureRecognizer {
    collectionView.panGestureRecognizer.require(toFail: rec)
}
複製代碼

總結

  • 如何查找第一響應者:
1.UIWindow 接受到一個事件,執行 hit-test 去尋找應該接受該事件的對象
二、hitTest:withEvent 將會經過調用 pointInside :withEvent: 方法來判斷
視圖是否包含當前事件的位置。
三、一直遞歸調用 hitTest:withEvent 直到找到最上層且包含 point 的對象,即爲第一響應者。
複製代碼
  • 事件不能處理的時響應鏈的傳遞過程
subview -> superview ... -> root view -> view controller -> root view controller -> window -> UIAPPlication - app delegate
複製代碼
  • UIControl 的子類處理事件的能力不收手勢識別器的影響

參考

相關文章
相關標籤/搜索