iOS事件傳遞和響應(yīng)機(jī)制

開場白

iOS開發(fā)這么多年,其實(shí)從來就沒關(guān)心過時(shí)間傳遞和響應(yīng)機(jī)制這么個(gè)事。當(dāng)我看到這篇文章史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-原理篇后,發(fā)現(xiàn)其中有很多東西可以細(xì)細(xì)品味一下的。

1.簡述事件流程

整個(gè)事件傳遞和處理流程,簡單概括為:

事件-事件傳遞到指定界面-找到可響應(yīng)的界面-響應(yīng)

我開始的理解誤區(qū)就是‘傳遞到指定界面’和‘可響應(yīng)界面’理解成同一個(gè)界面了,造成我在看上面的文章的時(shí)候,有些混亂。其實(shí)這兩個(gè)可以是兩個(gè)界面。

例如:我在touchBegin一個(gè)view的時(shí)候,需求是view不響應(yīng),而superview響應(yīng)。而事件傳遞是傳遞到view中。這種情況兩個(gè)view就是不相同的界面。

2.事件傳遞

  1. 當(dāng)有用戶觸摸屏幕的時(shí)候產(chǎn)生事件,系統(tǒng)硬件進(jìn)程獲取到這個(gè)事件,并處理封裝保存在系統(tǒng)中,由于系統(tǒng)硬件進(jìn)程和app進(jìn)程是兩個(gè)不同的進(jìn)程,所以使用進(jìn)程間的端口通信。
  2. 系統(tǒng)會將這個(gè)事件加入到UIApplication的事件管理隊(duì)列中,事件從隊(duì)列中出隊(duì)后通常會發(fā)送給app的keywindow處理。
  3. keywindow會找到一個(gè)最適合的視圖去處理事件。也就是從super控件到子控件中。
  4. 簡單總結(jié):UIApplication->window->尋找處理事件最合適的view

2.1 找到適合視圖的過程

  1. 首先keywindow是可以接受事件的
  2. 判斷是否事件發(fā)生在自己的可視范圍內(nèi),例如:觸摸點(diǎn)擊在自己的bound中。
  3. 子控件數(shù)組按照從后往前的順序查找適合的子控件,重復(fù)步驟1和步驟2。(從后往前的意思就是subviews中從最后一個(gè)元素開始向前找,這種方式可以減少遍歷次數(shù),提高效率)
  4. 找到子控件后再繼續(xù)找它的子控件。
  5. 如果沒有找到合適的子控件,那么當(dāng)前的控件就是最適合的。

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

  • 不允許交互:userInteractionEnabled = NO,例如UIImageView中addSubview一個(gè)button,button的點(diǎn)擊是沒有反應(yīng)的。
  • 隱藏:如果把父控件隱藏,那么子控件也會隱藏,隱藏的控件不能接受事件
  • 透明度:如果設(shè)置一個(gè)控件的透明度<0.01,會直接影響子控件的透明度。

如果不想讓view處理事件,而是想讓superview處理,就可以吧view的userInteractionEnabled設(shè)置為no。

2.3 最適合的子控件

系統(tǒng)api中提供了兩個(gè)方法,

- (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:方法在文章后續(xù)用hitTest代替,pointInside:withEvent:用pointInside代替

通過注釋了解到hitTest方法是遞歸的調(diào)用pointInside方法。point是在接受控件坐標(biāo)系內(nèi)的。

底層的事件傳遞實(shí)現(xiàn)就是:
產(chǎn)生觸摸事件->UIApplication事件隊(duì)列->[UIWindow hitTest:withEvent:]->返回更合適的view->[子控件 hitTest:withEvent:]->返回最合適的view->...->返回最合適的view

2.4 攔截事件傳遞

我們可以重寫hitTest方法,來攔截系統(tǒng)的事件傳遞,讓指定的view處理事件。例如自定義view中,想讓view中的一個(gè)subview處理事件,就可以在自定義view中重寫該方法:

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

示例代碼我返回的是self,這里可以改成指定的subview,或者遍歷subview中的一個(gè)。

3. 響應(yīng)鏈

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

3.1 UIResponder的子類

UIResponder的子類有一下幾個(gè):

  • AppDelegate
  • UIApplication
  • UIViewController
  • UIView

p.s. UIWindow的父類是UIView

3.2 nextResponder

UIResponder的子類是通過nextResponder進(jìn)行連接的。

響應(yīng)鏈創(chuàng)建方式,本人個(gè)人理解,應(yīng)該是鏈表的頭插法形式:

  1. AppDelegate作為整個(gè)鏈的根基,是第一個(gè)被創(chuàng)建出來的,在main函數(shù)中被調(diào)用。它的nextResponder為nil。當(dāng)前鏈表的狀態(tài):AppDelegate->nil
  2. 系統(tǒng)提供給我們的UIApplication單例,響應(yīng)鏈變?yōu)椋?strong>UIApplication->AppDelegate->nil
  3. UIApplication會創(chuàng)建keyWindow,是UIWindow類型,父類是UIView,也是UIResponder的子類,所以響應(yīng)鏈變?yōu)椋?strong>keyWindow->UIApplication->AppDelegate->nil
  4. keyWindow中會設(shè)置一個(gè)rootViewController,是UIViewController類型,是UIResponder子類,rootViewController->keyWindow->UIApplication->AppDelegate->nil
  5. rootViewController中有view,我們在開發(fā)中把自定義的view加載vc的view中,最終響應(yīng)鏈為:自定義view->superview->rootViewController->keyWindow->UIApplication->AppDelegate->nil

這里只是簡單舉個(gè)例子,其實(shí)項(xiàng)目中會有更復(fù)雜的層級關(guān)系。

3.3 官方文檔可以證明

很多人會問如何證明呢,我們來看看官方文檔中的解釋:


Summary

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

返回響應(yīng)者鏈中的下一個(gè)響應(yīng)者,如沒有下一個(gè)響應(yīng)者返回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類不會自動存儲和設(shè)置下一個(gè)響應(yīng)者(next responder),這個(gè)方法默認(rèn)返回nil。子類必須復(fù)寫這個(gè)方法并且返回一個(gè)合適的下一個(gè)響應(yīng)者。例如,UIView實(shí)現(xiàn)這個(gè)方法,如果是被UIViewController對象管理的下一個(gè)響應(yīng)者就是UIViewController;如不哦不是被UIViewController對象管理的,下一個(gè)響應(yīng)者就是superview。UIViewController同樣實(shí)現(xiàn)這個(gè)方法,并且返回它自己view的superview。UIWindow返回application對象。shared UIApplication對象通常返回nil,但是如果該對象是一個(gè)UIRespnder的子類并且還沒有被調(diào)用去處理事件,它返回的是app的delegate。

3.4 事件響應(yīng)鏈中的傳遞

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

如果多層有實(shí)現(xiàn)了UIResponder的相關(guān)方法,例如touchesBegan,這多層都可以響應(yīng)。

舉個(gè)例子:
vc中init一個(gè)自定義的TestView,并且在vc和TestView中都實(shí)現(xiàn)了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];
}

運(yùn)行后的效果:


點(diǎn)擊紅色區(qū)域后查看控制臺:



TestView和VC的touchesBegan方法都調(diào)用了。

注意:TestView中的touchesBegan要調(diào)用super touchesBegan,如果不調(diào)用,vc中無法打印。因?yàn)椴徽{(diào)用就不會繼續(xù)查找響應(yīng)鏈中后續(xù)的響應(yīng)者了。vc中touchesBegan中調(diào)用了super也是同理目的。

4. 簡單總結(jié)

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

5. 應(yīng)用場景

參考這篇文章:iOS事件響應(yīng)鏈中hitTest的應(yīng)用示例

其中包括:

  • 擴(kuò)大UIButton的響應(yīng)熱區(qū)
  • 子view超出了父view的bounds響應(yīng)事件
  • 使部分區(qū)域失去響應(yīng).
  • 讓非scrollView區(qū)域響應(yīng)scrollView拖拽事件
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容