1. 程式人生 > >UIResponder響應鏈

UIResponder響應鏈

開啟 進行 return member 字符 tro for blog list

一、概述   UIView與UIViewController的共同父類:UIResponder,對於點擊touches一系列方法,UIView與UIViewController會做出一系列反應,下面從“如何找到點擊的子view”和“如何根據響應鏈響應”兩方面來認識UIResponder。 二、 如何找到點擊的子view   當用戶點擊某一個視圖或者按鈕的時候會首先響應application中UIWindow一層一層的向下查找,直到找到用戶指定的view為止。 技術分享   比如上圖,點擊ViewE,會首先響應application中UIWindow一層一層的向下查找。查到ViewA,發現點擊位置在ViewA內,接下來發現點擊位置在ViewB內,接下來發現點擊位置在ViewE內,就找到了ViewE。主要通過下面兩個方法來找到的:
- (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響應鏈