iOS事件傳遞和響應機制

開場白

iOS開發這麼多年,其實歷來就沒關心過期間傳遞和響應機制這麼個事。當我看到這篇文章史上最詳細的iOS之事件的傳遞和響應機制-原理篇後,發現其中有不少東西能夠細細品味一下的。ios

1.簡述事件流程

整個事件傳遞和處理流程,簡單歸納爲:api

事件-事件傳遞到指定界面-找到可響應的界面-響應數組

我開始的理解誤區就是‘傳遞到指定界面’和‘可響應界面’理解成同一個界面了,形成我在看上面的文章的時候,有些混亂。其實這兩個能夠是兩個界面。markdown

例如:我在touchBegin一個view的時候,需求是view不響應,而superview響應。而事件傳遞是傳遞到view中。這種狀況兩個view就是不相同的界面。app

2.事件傳遞

  1. 當有用戶觸摸屏幕的時候產生事件,系統硬件進程獲取到這個事件,並處理封裝保存在系統中,因爲系統硬件進程和app進程是兩個不一樣的進程,因此使用進程間的端口通訊。
  2. 系統會將這個事件加入到UIApplication的事件管理隊列中,事件從隊列中出隊後一般會發送給app的keywindow處理。
  3. keywindow會找到一個最適合的視圖去處理事件。也就是從super控件到子控件中。
  4. 簡單總結:UIApplication->window->尋找處理事件最合適的view

2.1 找到適合視圖的過程

  1. 首先keywindow是能夠接受事件的
  2. 判斷是否事件發生在本身的可視範圍內,例如:觸摸點擊在本身的bound中。
  3. 子控件數組按照從後往前的順序查找適合的子控件,重複步驟1和步驟2。(從後往前的意思就是subviews中從最後一個元素開始向前找,這種方式能夠減小遍歷次數,提升效率)
  4. 找到子控件後再繼續找它的子控件。
  5. 若是沒有找到合適的子控件,那麼當前的控件就是最適合的。

2.2 UIView不能接收觸摸事件的三種狀況

  • 不容許交互:userInteractionEnabled = NO,例如UIImageView中addSubview一個button,button的點擊是沒有反應的。
  • 隱藏:若是把父控件隱藏,那麼子控件也會隱藏,隱藏的控件不能接受事件
  • 透明度:若是設置一個控件的透明度<0.01,會直接影響子控件的透明度。

若是不想讓view處理事件,而是想讓superview處理,就能夠吧view的userInteractionEnabled設置爲no。ide

2.3 最適合的子控件

系統api中提供了兩個方法,函數

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds
複製代碼

爲了方便:hitTest:withEvent:方法在文章後續用hitTest代替,pointInside:withEvent:用pointInside代替oop

經過註釋瞭解到hitTest方法是遞歸的調用pointInside方法。point是在接受控件座標系內的。this

底層的事件傳遞實現就是: 產生觸摸事件->UIApplication事件隊列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view->...->返回最合適的viewspa

2.4 攔截事件傳遞

咱們能夠重寫hitTest方法,來攔截系統的時間傳遞,讓指定的view處理事件。例如自定義view中,想讓view中的一個subview處理事件,就能夠在自定義view中重寫該方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if ([self pointInside:point withEvent:event]) {
        return self;
    }
    
    return nil;
}
複製代碼

示例代碼我返回的是self,這裏能夠改爲指定的subview,或者遍歷subview中的一個。

3. 響應鏈

在不少文章中都看到了這張圖,不清楚是否是官方,但圖片中的邏輯是沒有問題的,ios控件間的擺放都是有層級關係的,這張圖表示的很清晰。響應者對象就是繼承與UIResponder的子類們。

3.1 UIResponder的子類

UIResponder的子類有一下幾個:

  • AppDelegate
  • UIApplication
  • UIViewController
  • UIView

p.s. UIWindow的父類是UIView

3.2 nextResponder

UIResponder的子類是經過nextResponder進行鏈接的。

響應鏈建立方式,本人我的理解,應該是鏈表的頭插法形式:

  1. AppDelegate做爲整個鏈的根基,是第一個被建立出來的,在main函數中被調用。它的nextResponder爲nil。當前鏈表的狀態:AppDelegate->nil
  2. 系統提供給咱們的UIApplication單例,響應鏈變爲:UIApplication->AppDelegate->nil
  3. UIApplication會建立keyWindow,是UIWindow類型,父類是UIView,也是UIResponder的子類,因此響應鏈變爲:keyWindow->UIApplication->AppDelegate->nil
  4. keyWindow中會設置一個rootViewController,是UIViewController類型,是UIResponder子類,rootViewController->keyWindow->UIApplication->AppDelegate->nil
  5. rootViewController中有view,咱們在開發中把自定義的view加載vc的view中,最終響應鏈爲:自定義view->superview->rootViewController->keyWindow->UIApplication->AppDelegate->nil

這裏只是簡單舉個例子,其實項目中會有更復雜的層級關係。

3.3 官方文檔能夠證實

不少人會問如何證實呢,咱們來看看官方文檔中的解釋:

Summary

Returns the next responder in the responder chain, or nil if there is no next responder.

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

Disussion

The UIResponder class does not store or set the next responder automatically, so this method returns nil by default. Subclasses must override this method and return an appropriate next responder. For example, UIView implements this method and returns the UIViewController object that manages it (if it has one) or its superview (if it doesn’t). UIViewController similarly implements the method and returns its view’s superview. UIWindow returns the application object. The shared UIApplication object normally returns nil, but it returns its app delegate if that object is a subclass of UIResponder and has not already been called to handle the event.

UIResponder類不會自動存儲和設置下一個響應者(next responder),這個方法默認返回nil。子類必須複寫這個方法而且返回一個合適的下一個響應者。例如,UIView實現這個方法,若是是被UIViewController對象管理的下一個響應者就是UIViewController;如不不是被UIViewController對象管理的,下一個響應者就是superview。UIViewController一樣實現這個方法,而且返回它本身view的superview。UIWindow返回application對象。shared UIApplication對象一般返回nil,可是若是該對象是一個UIRespnder的子類而且尚未被調用去處理事件,它返回的是app的delegate。

3.4 事件響應鏈中的傳遞

經過上面例子中的響應鏈自定義view->superview->rootViewController->keyWindow->UIApplication->AppDelegate->nil的順序,逐層向後查找可作響應的響應者(UIResponder子類)。

若是多層有實現了UIResponder的相關方法,例如touchesBegan,這多層均可以響應。

舉個例子: vc中init一個自定義的TestView,而且在vc和TestView中都實現了touchesBegan方法

vc部分代碼:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.view.backgroundColor = UIColor.lightGrayColor;
    
    TestView *view1 = [[TestView alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
    view1.tag = 1;
    view1.backgroundColor = [UIColor redColor];
    [self.view addSubview:view1];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    [super touchesBegan:touches withEvent:event];
}
複製代碼

TestView部分代碼:

@implementation TestView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    [super touchesBegan:touches withEvent:event];
}
複製代碼

運行後的效果: 點擊紅色區域後查看控制檯: TestView和VC的touchesBegan方法都調用了。

注意:TestView中的touchesBegan要調用super touchesBegan,若是不調用,vc中沒法打印。由於不調用就不會繼續查找響應鏈中後續的響應者了。vc中touchesBegan中調用了super也是同理目的。

4. 簡單總結

事件的傳遞和響應的區別: 事件的傳遞是從上到下(父控件到子控件),事件的響應是從下到上(順着響應者鏈條向上傳遞:子控件到父控件)。

5. 應用場景

參考這篇文章:iOS事件響應鏈中hitTest的應用示例

其中包括:

  • 擴大UIButton的響應熱區
  • 子view超出了父view的bounds響應事件
  • 使部分區域失去響應.
  • 讓非scrollView區域響應scrollView拖拽事件
相關文章
相關標籤/搜索