UIResponder響應鏈
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver‘s coordinate system - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds
UIWindow會通過調用hitTest:withEvent:方法(這個方法會對UIWindow的所有子view調用pointInside:withEvent:方法,其中返回的YES的view為ViewA,得知用戶點擊的範圍在ViewA中),類似地,在ViewA中調用hitTest:withEvent:方法,得知用戶點擊的範圍在ViewB中,依此類推,最終找到點擊的view為ViewE。
其中,hitTest:withEvent:方法大致的實現如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ for (UIView *view in self.subviews) { if([view pointInside:point withEvent:event]){ UIView *hitTestView = [view hitTest:point withEvent:event]; if(nil == hitTestView){return view; } } } return nil; }
通過以上這種遞歸的形式就能找到用戶點擊的是哪個view,其中還要註意的時當前的view是否開啟了userIntercationEnabled屬性,如果這個屬性未開啟,以上遞歸也會在未開啟userIntercationEnabled屬性的view層終止。
三、如何根據響應鏈響應
既然找到了用戶點擊的view,那麽當前就應該響應用戶的點擊事件了,UIView與UIViewController的共同父類是UIResponder,他們都可以復寫下列4個方法:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
這個響應點擊事件的過程是上面的逆序操作,這就是用到了UIResponder的nextResponder方法了。比如上面的圖點擊ViewE,這時候ViewE先響應,接下來是nextResponder即ViewB,接下來是ViewB的nextResponder即ViewA。
關於nextResponder有如下幾條規則:
1. 當一個view調用其nextResponder會返回其superView;
2. 如果當前的view為UIViewController的view被添加到其他view上,那麽調用nextResponder會返回當前的UIViewController,而這個UIViewController的nextResponder為view的superView;
3. 如果當前的UIViewController的view沒有添加到任何其他view上,當前的UIViewController的nextResponder為nil,不管它是keyWinodw或UINavigationController的rootViewController,都是如此;
4. 如果當前application的keyWindow的rootViewController為UINavigationController(或UITabViewController),那麽通過調用UINavigationController(或UITabViewController)的nextResponder得到keyWinodw;
5. keyWinodw的nextResponder為UIApplication,UIApplication的nextResponder為AppDelegate,AppDelegate的nextResponder為nil。
用圖來表示,如下所示:
四、遇到的問題
在開發過程中,我們有可能遇到UIScrollView 或 UIImageView 截獲touch事件,導致touchesBegan: withEvent:/touchesMoved: withEvent:/touchesEnded: withEvent: 等方法不執行。比如下面這種情況,scrollView的superView是view,view對應的viewController中的touchesBegan: withEvent:/touchesMoved: withEvent:/touchesEnded: withEvent: 等方法就不執行。
如果想讓viewController中的方法執行的話,你可能會提出下面的解決辦法:@implementation UIScrollView (Touch) - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { if([self isMemberOfClass:[UIScrollView class]]) { [[self nextResponder] touchesBegan:touches withEvent:event]; } } @end
這樣UIScrollView確實會用nextResponder把響應傳遞到view,接下來傳遞到viewController中。但是,如果沒有使用if([self isMemberOfClass:[UIScrollView class]]) 進行過濾判斷,那麽,有可能會導致一個使用系統手寫輸入法時帶來的crash問題。即手寫的鍵盤的子view是UIKBCandidateCollectionView,調用了[[self nextResponder] touchesBegan:touches withEvent:event];後會造成系統的crash問題:
-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance 0x104f6c6b0
這個crash的復現見《UIKBBlurredKeyView candidateList:unrecognized...BUG修復》,它的解決辦法也隨處可見,比如《-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance 0x5a89960》
此crash的技術層面詳細原因:
手寫的鍵盤的子view是UIKBCandidateCollectionView(UIColloectionView的子類)的實例,它的nextResponder是UIKBHandwritingCandidateView類型的實例,執行UIKBHandwritingCandidateView的touchesBegan:withEvent:方法後,會使得整個candidate view呈選中狀態,而蘋果對手寫鍵盤的選擇candidate字符時的原生處理方法是會避免candidate view呈選中狀態的。整個candidate view呈選中狀態後後再點擊鍵盤的任意地方,本應調用UIKBCandidateView實例的方法candidateList,結果調用了UIKBBlurredKeyView的candidateList方法,導致方法找不到,導致"-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance "crash。
crash總結:
通過對這個crash的詳細分析,雖然上面的使用isMemberOfClass判斷後使用nextResponder對事件響應鏈進行傳遞沒有問題,但由於nextResponder依然具有不可控性,還是不建議用category復寫系統的方法,這一點以後一定註意。
謹慎使用Category,特別是覆蓋系統原始方法的category的實現。
UIResponder響應鏈