iOS7下滑動返回與ScrollView共存二三事

NSTopGun發表於2014-05-01

【轉載請註明出處】 = =不是整篇複製就算註明出處了親。。。

 

iOS7下滑動返回與ScrollView共存二三事

 

【前情回顧】

去年的時候,寫了這篇帖子iOS7滑動返回。文中提到,對於多頁面結構的應用,可以替換interactivePopGestureRecognizer的delegate以統一管理應用中所有頁面滑動返回的開關,比如在UINavigationController的派生類中

 1 //我是一個NavigationController的派生類
 2 - (id)initWithRootViewController:(UIViewController *)rootViewController
 3 {
 4     self = [super initWithRootViewController:rootViewController];
 5     if (self)
 6     {
 7         //在naviVC中統一處理棧中各個vc是否支援滑動返回的情況
 8         //當前僅最底層的vc關閉滑動返回
 9         self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
10     }
11     
12     return self;
13 }

然後在委託中控制滑動返回的開關

 1 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
 2 {
 3     if (self.viewControllers.count == 1)//關閉主介面的右滑返回
 4     {
 5         return NO;
 6     }
 7     else
 8     {
 9         return YES;
10     }
11 }

 

【問題所在】

看上去挺美好的。。。。直到遇到了ScrollView。

替換了delegate後,在使用時ScrollView,在螢幕左邊緣就無法觸發滑動返回效果了,如圖

 

【問題原因】

滑動返回事實上也是由存在已久的UIPanGestureRecognizer來識別並響應的,它直接與UINavigationController的view(方便起見,下文中以UINavigationController.view表示)進行繫結,因此上圖中存在如下關係:
UIPanGestureRecognizer          ——bind——  UIScrollView

UIScreenEdgePanGestureRecognizer ——bind——  UINavigationController.view

滑動返回無法觸發,說明UIScreenEdgePanGestureRecognizer並沒有接收到手勢事件。

 

根據apple君的官方文件,UIGestureRecognizer和UIView是多對一的關係(具體點這裡),UIGestureRecognizer一定要和view進行繫結才能發揮作用。因此不難想象,UIGestureRecognizer對於螢幕上的手勢事件,其接收順序和UIView的層次結構是一致的。同樣以上圖為例

 

(我是Z軸)-------------------------------------------------------------------------------------------------------------------------------------->

 

UINavigationController.view —>  UIViewController.view —>  UIScrollView —>  Screen and User's finger

 

即UIScrollView的panGestureRecognizer先接收到了手勢事件,直接就地處理而沒有往下傳遞。

 

實際上這就是兩個panGestureRecognizer共存的問題。

 

【解決方案】

蘋果以UIGestureRecognizerDelegate的形式,支援多個UIGestureRecognizer共存。其中的一個方法是:

1 // called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
2 // return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
3 //
4 // note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
5 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

 一句話總結就是此方法返回YES時,手勢事件會一直往下傳遞,不論當前層次是否對該事件進行響應。

 

看看UIScrollView的標頭檔案,有如下描述:

1 // Use these accessors to configure the scroll view's built-in gesture recognizers.
2 // Do not change the gestures' delegates or override the getters for these properties.
3 @property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);

UIScrollView本身是其panGestureRecognizer的delegate,且apple君明確表明不能修改它的delegate(修改的時候也會有警告)。

那麼只好曲線救國。

UIScrollView作為delegate,說明UIScrollView中實現了上文提到的shouldRecognizeSimultaneouslyWithGestureRecognizer方法,返回了NO。建立一個UIScrollView的category,由於category中的同名方法會覆蓋原有.m檔案中的實現,使得可以自定義手勢事件的傳遞,如下:

 1 @implementation UIScrollView (AllowPanGestureEventPass)
 2 
 3 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
 4 {
 5     if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]
 6         && [otherGestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]])
 7     {
 8         return YES;
 9     }
10     else
11     {
12         return  NO;
13     }
14 }

再次執行demo,看看效果:

嗯,滑動返回已經成功觸發,鼓掌!

等會等會。。。

好像不太對。。。

scrollView怎麼也滾動了!!!!!

O。。。只是做到了將手勢事件往下傳遞,而沒有關閉掉在邊緣時UIScrollView對事件的響應。

 

事實上,對UIGestureRecognizer來說,它們對事件的接收順序和對事件的響應是可以分開設定的,即存在接收鏈和響應鏈。接收鏈如上文所述,和UIView繫結,由UIView的層次決定接收順序。

而響應鏈在apple君的定義下,邏輯出奇的簡單,只有一個方法可以設定多個gestureRecognizer的響應關係:

// create a relationship with another gesture recognizer that will prevent this gesture's actions from being called until otherGestureRecognizer transitions to UIGestureRecognizerStateFailed
// if otherGestureRecognizer transitions to UIGestureRecognizerStateRecognized or UIGestureRecognizerStateBegan then this recognizer will instead transition to UIGestureRecognizerStateFailed
// example usage: a single tap may require a double tap to fail
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;

每個UIGesturerecognizer都是一個有限狀態機,上述方法會在兩個gestureRecognizer間建立一個依託於state的依賴關係,當被依賴的gestureRecognizer.state = failed時,另一個gestureRecognizer才能對手勢進行響應。

 

所以,只需要

[_scrollView.panGestureRecognizer requireGestureRecognizerToFail:screenEdgePanGestureRecognizer];

即可。

 

再次執行demo,看看效果:

 Works like a Charm!!

 

P.S:screenEdgePanGestureRecognizer是和UINavigationController.view繫結的,因此可以遍歷UINavigationController.view.gestureRecognizers來獲取,如下:

- (UIScreenEdgePanGestureRecognizer *)screenEdgePanGestureRecognizer
{
    UIScreenEdgePanGestureRecognizer *screenEdgePanGestureRecognizer = nil;
    if (self.view.gestureRecognizers.count > 0)
    {
        for (UIGestureRecognizer *recognizer in self.view.gestureRecognizers)
        {
            if ([recognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]])
            {
                screenEdgePanGestureRecognizer = (UIScreenEdgePanGestureRecognizer *)recognizer;
                break;
            }
        }
    }

    return screenEdgePanGestureRecognizer;
}

 


demo地址:https://github.com/cDigger/CoExistOfScrollViewAndBackGesture/tree/master

 

【總結】

寫了這麼多,只是為了最初統一管理滑動返回的一點點便利,似乎很有些得不償失。

我並不建議直接在專案中使用這種非常規手段。

但使用apple君提供的積木,自己拼出系統中的新功能,也是iOS開發的樂趣之一啊。

 

 

 

相關文章