隔離導航控制器帶來的坑(iOS)

題外話:最近一直很閑,項目上基本沒有啥大的需求。對于程序員來說,如果沒有需求其實是一件很難受的事情,之前好多次在項目中沒事找事,該優化的優化,該整理的整理??赡芎枚喑绦騿T都遇到過與我類似的情況。但是程序員真的需要通過刷項目去提高自己嗎?程序員的功力體現在處理項目的細節上,有可能為了改進一點點的體驗就要付出很大的代價,就要接觸更多的知識點。做的項目多,但是不精那么只能代表自己在比較淺顯的領域比較熟練而已。以上觀點是我在最近思考得來的,并不一定正確,甚至明天一早我就會推翻這種想法,請大家謹慎參考??。

一、什么是隔離導航控制器?

這個名詞純粹是我自己瞎編的,我不知道有沒有這種說法,至少我沒有看到。我所說的隔離導航控制器是指:在導航欄樣式不同的頁面采用不同的導航控制器?,F在同一個導航欄發生變化主要體現在:隱藏導航欄頁面跳轉到非隱藏導航欄的頁面、A顏色導航欄的頁面跳轉到B顏色導航欄的頁面、可跟隨滑動做動畫的頁面跳轉到固定導航欄的頁面。隔離導航控制器就是:當頁面跳轉到導航欄不同的頁面時不再使用同一個導航控制器,而是彈出一個新的導航控制器,這個導航控制器的導航欄固定不變,如果發生變化那么再彈出一個新的導航控制器。

二、為什么要隔離導航控制器?

很簡單,因為我已經受夠了頁面的來回跳轉導致狀態欄儲存、變化、恢復這樣的過程。尤其在側滑或者全屏滑動返回時,導航欄的變化會有不好的體驗。所以在很久以前我就在醞釀,如果present出來一個導航控制器就好了(隔離導航控制器并不是一個很好地解決辦法,但是可以試一試,畢竟我很閑)。因此我在項目中試了一下,結果遇到了一些問題,自己挖的坑,跪著也要填滿了。

三、實現右進右出的present

ViewController 的present樣式一共有4種:

typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
    UIModalTransitionStyleCoverVertical = 0,//默認
    UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,//翻轉
    UIModalTransitionStyleCrossDissolve,//透明度漸變
    UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,//翻書
};
[vc setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];    //present 前加上這句就行
[self presentViewController:vc animated:YES completion:nil];

沒有我們需要的效果,因此我們需要利用iOS 7 以后的轉場動畫來實現。說到轉場動畫不怕大家笑話,我已經看了好多遍了,但是依然記不住那些名字。每次要用都要去大神的博客里再學習一邊。我的代碼也是使用大神的代碼,因為覺得沒有必要再寫一遍。只不過我做了略微的修改。

// NormalDismissAnimation
// 4. Do animate now
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    toVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是為了上下兩個控制器有聯動的效果
    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        fromVC.view.frame = finalFrame;
        toVC.view.transform = CGAffineTransformIdentity;
 
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
 
 
//BouncePresentAnimation
  // 4. Do animate now
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        toVC.view.frame = finalFrame;
        fromVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是為了有上下兩個控制器聯動的效果
    } completion:^(BOOL finished) {
        fromVC.view.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:YES];
    }];
 
 
//SwipeUpInteractiveTransition
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
       switch (gestureRecognizer.state) {
        case UIGestureRecognizerStateBegan:
            // 1. Mark the interacting flag. Used when supplying it in delegate.
            self.interacting = YES;
            [self.presentingVC dismissViewControllerAnimated:YES completion:nil];
            break;
        case UIGestureRecognizerStateChanged: {
            // 2. Calculate the percentage of guesture
            CGSize screenSize = [UIScreen mainScreen].bounds.size;
            CGFloat fraction = translation.x / screenSize.width;
            //Limit it between 0 and 1
            fraction = fminf(fmaxf(fraction, 0.0), 1.0);
            self.shouldComplete = (fraction > 0.4);
 
            [self updateInteractiveTransition:fraction];
            break;
        }
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            // 3. Gesture over. Check if the transition should happen or not
            self.interacting = NO;
            if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
                [self cancelInteractiveTransition];
            } else {
                [self finishInteractiveTransition];
            }
            break;
        }
        default:
            break;
    }
}

以上部分很簡單而且并不是我們所關心的部分(雖然名字有點難記憶,但是不難理解)。完成以上代碼,就可以實現滑動返回+右進右出的present樣式了。

//還有一點需要注意的是,需要禁止全屏手勢在導航控制器棧里有多個元素時響應
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    if ([self.presentingVC isKindOfClass:[BaseNavigationController class]]) {
        BaseNavigationController *nav = (BaseNavigationController*)self.presentingVC;
        if (nav.viewControllers.count >=2) {
            return NO;
        }
    }
    return YES;
}

四、遇到的問題

我們(或者說大多數APP)都有這樣的需求,當收到推送時需要從底部彈出一個ViewController。 但是因為我們是present 出來了一些頁面,當收到推送的時候我們并不能一下子知道從哪個ViewController 上present 出來一個需要用戶響應的控制器。比方說 HomeViewController ---(右進右出present)---->ReportMessageViewController后,從代碼的角度我們不能一下子拿到展現在用戶面前的控制器(因為你不知道是否已經present了,也不知道誰調用的present)。因此我遇到了第一個問題(很多的項目可以通過tab、nav 來確定,而且present 出控制器的場景比較固定):

1、到底誰是當前正在響應的控制器?

要想解決這個問題我們首先要知道響應者鏈條。即當我們點擊了屏幕上的一個按鈕,事件是怎么傳遞的。UIView 和 UIViewController 都是繼承自UIResponder的,他們都可以成為響應者。因此我們只要遍歷屏幕上最上方的那些View(葉子節點),縱向循環找到他們的nextResponder,直至找到為UIViewController類的響應者。

// 獲取某個view 的葉子 View(一般為Window)
/*
 
*/
+(NSMutableArray *)getTopSubViewsWithParentView:(UIView *)rootView{
    NSMutableArray *stack = [NSMutableArray array];
    NSMutableArray *leafNodes = [NSMutableArray array];//存放葉子節點
    if (rootView.subviews.count == 0) {
        return nil;
    }
    [stack addObjectsFromArray:rootView.subviews];//把根視圖的所有第一層子視圖入棧
    while (stack.count != 0) {
        UIView *subView = [stack lastObject];//取出頂部元素并判斷是否為葉子節點
        [stack removeLastObject];
        if (subView.subviews.count != 0) {//不是葉子節點的話將其子視圖繼續入棧
            [stack addObjectsFromArray:subView.subviews];
        }else{
            [leafNodes addObject:subView];//如果是葉子節點則將其入棧(葉子節點的棧)
        }
    }
    return leafNodes;
}
 
//獲取某個視圖在哪個控制器上
+(UIViewController*)getViewControllerWithView:(UIView*)view{
     
    UIResponder *res = view;
    while (res) {
        if (res.nextResponder) {
            res = res.nextResponder;
        }
        if ([res isKindOfClass:[UIViewController class]]) {
             
            UIViewController *vc = (UIViewController*)res;
            return vc;
        }
    }
    return nil;
}
 
//這段代碼的意思是,如果我能判斷的更精確就精確些。比如某個導航控制器,你說他在響應也行,他的top元素在響應也行,顯然我想精確到top元素
+(UIViewController*)getCurrentVC{
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
     
    NSMutableArray *array = [self getTopSubViewsWithParentView:keyWindow];
    UINavigationController *nav = nil;
    UITabBarController *tab = nil;
    for (UIView *subView in array) {
         
        UIViewController *vc = [self getViewControllerWithView:subView];
        if (!([vc isKindOfClass:[UINavigationController class]] || [vc isKindOfClass:[UITabBarController class]])) {
            return vc;
        }
        if ([vc isKindOfClass:[UINavigationController class]]) {
            nav = (UINavigationController*)vc;
        }
        if ([vc isKindOfClass:[UITabBarController class]]) {
            tab = (UITabBarController *)vc;
        }
    }
    if (nav) {
        return nav;
    }
    if (tab) {
        return tab;
    }
    return nil;
}

有了這些代碼,問題一就解決了。

2、全屏滑動遇到了可以左右滑動的ScrollView怎么辦?

首先說明一下,這個問題我解決的并不完美,因為去除了bounces 效果。因為滑動ScrollView時,ScrollView 的pan 手勢會優先響應,并阻止其他手勢響應。首先我們先看一下手勢代理,看看都有哪些方法:

 1 @protocol UIGestureRecognizerDelegate <NSObject>
 2 @optional
 3 // called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed
 4 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
 5 
 6 // called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
 7 // return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
 8 //
 9 // 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
10 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;//手勢1、手勢2 是否可以共存,即兩者都響應,收到事件繼續傳遞下去
11 
12 // called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
13 // return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
14 //
15 // note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES
16 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);//在other 響應的情況下,自己是否不響應
17 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
18 
19 // called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
20 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
21 
22 // called before pressesBegan:withEvent: is called on the gesture recognizer for a new press. return NO to prevent the gesture recognizer from seeing this press
23 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

通過打印scrollView 的pan手勢我們會發現 pan手勢的代理是scrollView ,我嘗試過改變pan 的delegate 但是發現會崩潰,apple 是不允許改變這個delegate 的。所以我想到了寫一個UIScrollView的子類。

//
//  PanScrollView.m
//  Property
//
//  Created by 高雅馨on 16/7/25.
//  Copyright ? 2016年  高雅??. All rights reserved.
//
 
#import "PanScrollView.h"
 
@implementation PanScrollView
 
-(instancetype)init{
    if (self = [super init]) {
        self.bounces = NO;
    }
    return self;
}
 
/*
 是否將相應傳遞給other
  
  
 當偏移量X值為0的時候全屏手勢和pan 手勢同時響應。全屏手勢向右滑動時 ,由于bounces效果已經被去掉了,所以偏移量不變,ViewController 只有dismiss 效果。當向左滑動時全屏手勢沒有任何效果,只要scroll效果。
 */
 
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
     
    if (gestureRecognizer == self.panGestureRecognizer  && otherGestureRecognizer == self.otherGes){
        if (self.contentOffset.x==0) {
            return YES;
        }
    }
    return NO;
}
 
@end

至此,大部分可見的問題解決了,可能并不完美,也不適合,但是嘗試也是一種美好的回憶,希望能對大家有幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,255評論 4 61
  • 軌道運營交通線路維修,設備檢查環節是維修質量的一個非常重要的流程。檢查分手工檢查和儀器檢查。 手工檢查,主要是檢查...
    155守時待命閱讀 354評論 0 0
  • 我愿意 在夜晚 與月亮眉目傳情 卻不太想與人說話 我愿意 在清晨 聽小鳥在枝頭歌唱 卻不太想與人說話 我愿意 在林...
    吳森迪閱讀 254評論 0 0