iOS開發這麼多年,其實歷來就沒關心過期間傳遞和響應機制這麼個事。當我看到這篇文章史上最詳細的iOS之事件的傳遞和響應機制-原理篇後,發現其中有不少東西能夠細細品味一下的。ios
整個事件傳遞和處理流程,簡單歸納爲:api
事件-事件傳遞到指定界面-找到可響應的界面-響應數組
我開始的理解誤區就是‘傳遞到指定界面’和‘可響應界面’理解成同一個界面了,形成我在看上面的文章的時候,有些混亂。其實這兩個能夠是兩個界面。markdown
例如:我在touchBegin一個view的時候,需求是view不響應,而superview響應。而事件傳遞是傳遞到view中。這種狀況兩個view就是不相同的界面。app
若是不想讓view處理事件,而是想讓superview處理,就能夠吧view的userInteractionEnabled設置爲no。ide
系統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
咱們能夠重寫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中的一個。
在不少文章中都看到了這張圖,不清楚是否是官方,但圖片中的邏輯是沒有問題的,ios控件間的擺放都是有層級關係的,這張圖表示的很清晰。響應者對象就是繼承與UIResponder的子類們。
UIResponder的子類有一下幾個:
p.s. UIWindow的父類是UIView
UIResponder的子類是經過nextResponder進行鏈接的。
響應鏈建立方式,本人我的理解,應該是鏈表的頭插法形式:
這裏只是簡單舉個例子,其實項目中會有更復雜的層級關係。
不少人會問如何證實呢,咱們來看看官方文檔中的解釋:
Returns the next responder in the responder chain, or nil if there is no next responder.
返回響應者鏈中的下一個響應者,如沒有下一個響應者返回nil。
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。
經過上面例子中的響應鏈自定義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也是同理目的。
事件的傳遞和響應的區別: 事件的傳遞是從上到下(父控件到子控件),事件的響應是從下到上(順着響應者鏈條向上傳遞:子控件到父控件)。
參考這篇文章:iOS事件響應鏈中hitTest的應用示例
其中包括: