響應鏈簡單來說,就是一系列的相互關聯的物件,從firstResponder開始,到application物件結束,如果firstResponder 無法響應事件,則交給nextResponder來處理,直到結束為止。iOS中很多型別的事件分發,都依賴於響應鏈;在響應鏈中,所有物件的基類都是UIResponder,也就是說所有能響應事件的類都是UIResponder的子類,UIApplication/UIView/UIViewController都是UIResponder的子類,這說明所有的Views,絕大部分Controllers(不用來管理View的Controller除外)都可以響應事件。
PS.CALayer 不是UIResponder的子類,這說明CALayer無法響應事件,這也是UIView和CALayer的重要區別之一。
上一篇文章講解到iOS中TouchEvent分發的第一步(如果您還沒有了解,可以參考上一篇部落格:iOS事件分發機制(一)),確定HitTestView,並且文章中也說明了,不管你的application當前設定是否忽略互動事件(application.beginIgnoringInteractionEvents) ,hitTest總會呼叫的,當然如果忽略了互動事件,之後的事件分發都不會呼叫了,事件會直接被廢棄掉。假定我們沒有忽略事件,如果hitTestView 無法處理這個事件,事件就通過響應鏈往上傳遞(hitTestView算是最早的Responder),直到找到一個可以處理的Responder為止。舉個例子,如果觸控通過hitTest確定的是一個View,而這個View沒有處理事件,則事件會傳送給nextResponder 去處理,通常是superView,有關nextResponder的事件傳遞過程,官方給出了一張很形象的圖,如下所示
PS.View處理事件的方式有手勢或者重寫touchesEvent方法或者利用系統封裝好的元件(UIControls)。
圖中所表示的正是nextResponder的查詢過程,兩種方式分別對應兩種app的架構,左邊的那種app架構比較簡單,只有一個VC,右邊的稍微複雜一些,但是尋找路線的原則是一樣的,先解釋一下,UIResponder本身是不會去儲存或者設定nextResponder的,所謂的nextResponder都是子類去實現的(這裡說的是UIView,UIViewController,UIApplication),關於nextResponder的值總結如下:
1、UIView的nextResponder是直接管理它的UIViewController(也就是VC.view.nextResponder=VC),如果當前View不是ViewController直接管理的View,則nextResponder是它的superView(view.nextResponder = view.superView)
2、UIViewController的nextResponder是它直接管理的View的superView(VC.nextResponder = VC.view.superView)
3、UIWindow的nextResponder是UIApplication
4、UIApplication的nextResponder是UIApplicationDelegate(官方文件說是nil)
我寫了一段程式碼,列印當前UIResponder的所有nextResponder,大家可以拿去試一下,程式碼很簡單,如下:
1 2 3 4 5 6 7 8 9 |
void STLogResponderChain(UIResponder *responder) { NSLog(@"------------------The Responder Chain------------------"); NSMutableString *spaces = [NSMutableString stringWithCapacity:4]; while (responder) { NSLog(@"%@%@", spaces, responder.class); responder = responder.nextResponder; [spaces appendString:@"----"]; } } |
然後我測試了一下,列印的日誌如下圖所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
UIButton ----STPView --------UIView ------------STPFeedViewController ----------------UIView --------------------UIView ------------------------_STWrapperViewController ----------------------------UIView --------------------------------UIView ------------------------------------STNavigationController ----------------------------------------STPWindow --------------------------------------------UIApplication ------------------------------------------------STPAppDelegate |
這樣比較清晰,大家也會直觀的看到nextResponder的查詢過程。
同樣,我們也得到了一個方法,去得到當前任意View的ViewController,實現思路就是 不斷的去找nextResponder,直到找到UIViewController為止(如果有一個的話),直接上程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
@interface UIView (STKit) /** * @abstract view's viewController if the view has one */ - (UIViewController *)viewController; /** * @abstract 遞迴查詢view的nextResponder,直到找到型別為class的Responder * * @param class nextResponder 的 class * @return 第一個滿足型別為class的UIResponder */ - (UIResponder *)nextResponderWithClass:(Class) class; /// 查詢firstResponder - (UIResponder *)findFirstResponder; @end @implementation UIView (STKit) /** * @abstract view's viewController if the view has one */ - (UIViewController *)viewController { return (UIViewController *)[self nextResponderWithClass:UIViewController.class]; } /** * @abstract 遞迴查詢view的nextResponder,直到找到型別為class的Responder * * @param class nextResponder 的 class * @return 第一個滿足型別為class的UIResponder */ - (UIResponder *)nextResponderWithClass:(Class) class { UIResponder *nextResponder = self; while (nextResponder) { nextResponder = nextResponder.nextResponder; if ([nextResponder isKindOfClass:class]) { return nextResponder; } } return nil; } - (UIResponder *)findFirstResponder { if (self.isFirstResponder) { return self; } for (UIView *subView in self.subviews) { id responder = [subView findFirstResponder]; if (responder) { return responder; } } return nil; } @end |
一般來說,我不太習慣在View中直接獲取VC或者NVC,我習慣代理的方式,但是做專案的過程中,不同Team的習慣不同,畢竟做專案和做研究還不是同一回事(以前的專案就是在任何地方你都可以通過當前View去獲取最近的NAV然後PushVC),所以大家用這個的時候可以綜合當前的編碼規範,大家可以拿上面的東西作為參考,如果寫的有不對的地方,可以指出。
接下來我們說正事了,假定我們現在有一個View是hitTestView,命名為 STImageView, 現在我們想讓這個image處理一些事情,比如所有的圖片點下之後加一個灰色的效果,我們就把事件分發給它。
在UIResponder中,提供以下幾個方法,幾個方法分別表示點選的不同狀態,大家看名字就能明白差不多:
- (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;
如果我們想讓我們當前的Responder處理事件,我們則需要重寫如下的幾個方法。我們的需求是手指按下圖片的時候加一個灰色的效果,鬆開的時候灰色消失。關於灰色的實現,我們暫定用一個View貼在ImageView上named maskView,然後用hidden來控制是否顯示(上一篇文章有說過,所有hidden的View預設不接受任何事件)。
我們需要在
touchesBegan方法裡面self.maskView.hidden = NO;
然後在
touchesEnded/Cancelled 裡面 self.maskView.hidden = YES;
就可以實現我們的效果了,原理很簡單,我們的hitTestView 在事件分發的時候去處理事件,僅此而已。這裡注意一下:UIImageView的預設是不接受點選事件的,如果想要實現如上所示效果,需要設定userInteractionEnabled=YES;
說到這裡,就有人產生了疑問,如果這麼實現的話,那如果本身UIImageView還想讓下面的View處理事件該怎麼辦?會不會把所有的事件攔截下來?這裡就說到了另一個問題,UIResponder在知道需要處理事件的時候,還是有決定權的,比如我可以決定讓整個響應鏈繼續走下去,或者直接中斷掉整個響應鏈。如果中斷了響應鏈,那麼所有在鏈上的nextResponder都不會得知有事件發生,iOS也提供了這個方法,其實很簡單:
我們在重寫TouchesEvents的時候,如果不想讓響應鏈繼續傳遞,就不呼叫super對應的實現就可以了,相反,有些時候你只需要做一個小改變,如上所示,但是你不想中斷響應鏈,你就需要呼叫父類對應的實現。
這裡有一點需要注意,一般來說,我們如果想要自己處理一些事件,我們需要重寫如上所示的方法,如果我們想自己處理,就不需要呼叫super。呼叫super的目的就是為了把事件傳遞給nextResponder,並且如果我們在 touchesBegan 中沒有呼叫super,則super不會響應其他的回掉(touchesMoved/touchesEnded),但是我們需要重寫所有如上所示的方法來確保我們的一切正常。touchesBegan 和 touchesEnded/touchesCancelled一定是成對出現的,這點大家可以放心。
有關觸控事件在響應鏈上的分發,就差不多這麼多東西,最重要的是大家可以看那幾個touches方法,多做實驗,就可以瞭解的更加深入。
這裡有一些補充,響應鏈能夠處理很多東西,不僅僅是觸控事件。一般來說,如果我們需要一個物件去處理一個非觸控事件(搖一搖,RemoteControlEvents,呼叫系統的複製、貼上框等),我們要確保該物件是UIResponder子類,如果我們要接收到事件的話,我們需要做兩件事情
1、重寫canBecomeFirstResponder ,並且返回YES
2、在需要的時候像該物件傳送becomeFirstResponder訊息。
我們有時候會遇到一些問題,比如我們重寫了motionEvents,但是我們不能收到搖一搖的回撥,或者我們的UIMenuController老是不彈出,我們就需要檢查一下,我們是否滿足瞭如上所示的條件,而且要確保becomeFirstResponder的傳送時機正確。
當然,這個補充對於觸控事件無效,觸控事件的第一響應者是根據hitTest確定而來的,有點繞,需要仔細捋捋。
需要注意的是:
如果你自己想自定義一個非TouchEvent的事件,當需要繼續傳遞事件的話,切記不要在實現內直接顯示的呼叫nextResponder的對應方法, 而是直接呼叫super對應的方法來讓這個事件繼續分發到響應鏈。
到目前為止,事件的分發還沒有結束,之後會有一篇文章介紹一個很重要的角色,手勢。
最後,附上官方的文件