iOS 響應(yīng)者及響應(yīng)者鏈

當(dāng)我們點(diǎn)擊一個 button 時,button 的響應(yīng)消息機(jī)制分為兩塊:

  • 首先在視圖層次中找到能響應(yīng)消息的那個視圖即 button;

  • 然后在找到的視圖 button 中進(jìn)行事件處理;

UIButton 繼承關(guān)系:

UIButton < UIControl < UIView < UIResponder

UIButton 之所以能夠處理事件,是因為它繼承自 UIResponder。也就是說只有繼承自UIResponder的類才能處理事件。

找響應(yīng)者

如圖,找響應(yīng)者是從父 View 到子 View 過程查找。主要用到了 UIView 的hitTest:withEvent: 以及 pointInside:withEvent: 兩個方法。

原理如下:

  • 當(dāng)用戶點(diǎn)擊屏幕時,會產(chǎn)生一個觸摸事件,系統(tǒng)會將該事件加入到一個由 UIApplication 管理的事件隊列中;

  • UIApplication 會從事件隊列中取出最前面的事件進(jìn)行分發(fā)以便處理,通常,先發(fā)送事件給應(yīng)用程序的主窗口(UIWindow);

  • 主窗口會調(diào)用hitTest:withEvent:方法在視圖(UIView)層次結(jié)構(gòu)中找到一個最合適的 UIView 來處理觸摸事件
    (hitTest:withEvent:其實是 UIView 的一個方法,UIWindow 繼承自 UIView,因此主窗口 UIWindow 也是屬于視圖的一種);

hitTest:withEvent: 方法處理機(jī)制:

當(dāng)前 view 調(diào)用自身的 pointInside: withEvent:方法判斷觸摸點(diǎn)是否在自己范圍內(nèi):

  • pointInside: withEvent:方法返回 NO,則說明觸摸點(diǎn)不在自己范圍內(nèi),則當(dāng)前 view 的hitTest: withEvent:方法返回 nil,當(dāng)前 view上 的所有 subview 都不做判斷。有點(diǎn)領(lǐng)導(dǎo)的意見一票否決的味道。

  • pointInside: withEvent:方法返回 YES,則說明觸摸點(diǎn)在自己的范圍內(nèi)。但無法判斷是否在自己身上還是在 subview 的身上。此時,遍歷所有的 subviews,對每個 subview 調(diào)用 hitTest 方法。這里要注意,遍歷的順序是從當(dāng)前 view 的 subviews 數(shù)組的尾部開始遍歷。因此離用戶最近的上層的 subview 會優(yōu)先被調(diào)用 hitTest 方法。

  • 一旦 hitTest 方法返回非空的 view,則被返回的 view 就是最終相應(yīng)觸摸事件的 view,尋找 hitTesting view 的階段到此結(jié)束,不再遍歷。
    若當(dāng)前 view 的所有 subviews 的 hitTest 方法都返回 nil,則當(dāng)前 view 的 hitTest 方法返回 self 作為最終的 hitTesting view,處理結(jié)束。

舉個例子,更加清晰的了解下:

如圖:

當(dāng)用戶點(diǎn)擊ViewD所在的區(qū)域時會進(jìn)行以下hit-Testing:

  • ViewA 的 pointInside 返回 YES,因為觸摸點(diǎn)在其 bounds 內(nèi)。遍歷 ViewA 的兩個 subview;

  • ViewB 的 pointInside 返回 NO,因為觸摸點(diǎn)不在其 bounds 內(nèi),ViewB 的 hitTest 方法返回 nil。而且發(fā)生一票否決,在 ViewB 上的所有 subviews 受到牽連將不再進(jìn)行 hit-Testing 處理。

  • ViewC 的 pointInside 返回 YES,因為觸摸點(diǎn)在其 bounds 范圍內(nèi),ViewC 的 hitTest 方法返回默認(rèn)處理,也就是 return [super hitTest:point withEvent:event]; 遍歷 ViewC 的兩個 subview;

  • ViewD的 pointInside 返回 YES,因為觸摸點(diǎn)在其 bounds 范圍內(nèi),且ViewD 沒有 subview,因此 hitTest 方法返回其自己。hitTesting view 找到,結(jié)束處理

需要注意的地方:

1、hitTest 方法調(diào)用 pointInside 方法;

2、hit-Testing 過程是從 superView 向 subView 逐級傳遞,也就是從層次樹的根節(jié)點(diǎn)向葉子節(jié)點(diǎn)傳遞;

3、遇到以下設(shè)置時,view 的 pointInside 將返回NO,hitTest 方法返回 nil:

  • view.isHidden=YES;
  • view.alpah<=0.01;
  • view.userInterfaceEnable=NO;
  • control.enable=NO;(UIControl的屬性)

事件響應(yīng)

在上一部分已經(jīng)找到了響應(yīng)者,這個響應(yīng)者就會執(zhí)行相應(yīng)的 touch 系列方法,系統(tǒng)默認(rèn)處理事件之后將不繼續(xù)向下一響應(yīng)者傳遞。我們自己可以根據(jù)需要,通過復(fù)寫方法把當(dāng)前事件向下一響應(yīng)者進(jìn)行傳遞。

可以復(fù)寫下列方法對事件進(jìn)行處理

//觸摸開始,手指觸碰屏幕
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//觸摸結(jié)束,手指離開屏幕
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//觸摸取消(如電話接入的時候)
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//手指移動(會調(diào)用多次)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//3D touch 9.1之后加入的3D觸摸事件
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches

舉個例子:

新建一個 Single View App,在 ViewController.m 中:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    //加上這一句,事件就可以向下一個響應(yīng)者傳遞
    [super touchesBegan:touches withEvent:event];
    
    NSLog(@"viewController touch begin");
}

在 AppDelegate.m 中:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"appDelegate touch begin");
}

這樣我們就實現(xiàn)了將事件向下一個響應(yīng)者傳遞。

響應(yīng)者鏈

下圖響應(yīng)者鏈鏈來自官網(wǎng)

我們也可以通過代碼打印響應(yīng)者鏈:

- (IBAction)click:(id)sender {
    UIResponder *res = sender;
    
    while (res) {
        NSLog(@"*************************************\n%@",res);
        res = [res nextResponder];
    }
}
  • UIView 的 nextResponder 屬性,如果有管理此 view 的 UIViewController 對象,則為此 UIViewController 對象;否則 nextResponder 即為其 superview。
  • UIViewController 的 nextResponder 屬性為其管理 view 的 superview.
  • UIWindow 的 nextResponder 屬性為 UIApplication 對象。
  • UIApplication 的 nextResponder 屬性為 nil。

解釋一下:

1、如果 hit-test view 或 first responder 不處理此事件,則將事件傳遞給其 nextResponder 處理,若有 UIViewController 對象則傳遞給 UIViewController,傳遞給其 superView。

2、如果 view 的 viewController 也不處理事件,則 viewController 將事件傳遞給其管理 view 的 superView。

3、視圖層級結(jié)構(gòu)的頂級為 UIWindow 對象,如果 window 仍不處理此事件,傳遞給 UIApplication.

4、若 UIApplication 對象不處理此事件,則事件被丟棄。

了解響應(yīng)者鏈有時候可以幫我解決一些實際問題。我舉個例子,我們知道,當(dāng)提供給你一個ViewController你可以很容易得到它的view,一句代碼的事情:

viewWanted = someViewController.view;

但如果反過來呢?當(dāng)給你一個view,讓你找到其所在的ViewController呢?這時候響應(yīng)者鏈可以幫上忙了,代碼如下:

@implementation UIView (FindController)
-(UIViewController*)parentController{
    UIResponder *responder = [self nextResponder];
    while (responder) {
    if ([responder isKindOfClass:[UIViewController class]]) {
        return (UIViewController*)responder;
    }
    responder = [responder nextResponder];
    }
    return nil;
}
@end

放一張完整的圖來理解下iOS觸摸事件的流動

參考資料:

官方文檔

https://www.cnblogs.com/Quains/p/3369132.html

https://www.cnblogs.com/wengzilin/p/4720550.html

http://shellhue.github.io/2017/03/04/FlowOfUITouch/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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