隔离导航控制器带来的坑(iOS)

题外话:最近一直很闲,项目上基本没有啥大的需求。对于程序员来说,如果没有需求其实是一件很难受的事情,之前好多次在项目中没事找事,该优化的优化,该整理的整理。可能好多程序员都遇到过与我类似的情况。但是程序员真的需要通过刷项目去提高自己吗?程序员的功力体现在处理项目的细节上,有可能为了改进一点点的体验就要付出很大的代价,就要接触更多的知识点。做的项目多,但是不精那么只能代表自己在比较浅显的领域比较熟练而已。以上观点是我在最近思考得来的,并不一定正确,甚至明天一早我就会推翻这种想法,请大家谨慎参考😄。

一、什么是隔离导航控制器?

这个名词纯粹是我自己瞎编的,我不知道有没有这种说法,至少我没有看到。我所说的隔离导航控制器是指:在导航栏样式不同的页面采用不同的导航控制器。现在同一个导航栏发生变化主要体现在:隐藏导航栏页面跳转到非隐藏导航栏的页面、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

至此,大部分可见的问题解决了,可能并不完美,也不适合,但是尝试也是一种美好的回忆,希望能对大家有帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,103评论 4 62
  • 轨道运营交通线路维修,设备检查环节是维修质量的一个非常重要的流程。检查分手工检查和仪器检查。 手工检查,主要是检查...
    155守时待命阅读 337评论 0 0
  • 我愿意 在夜晚 与月亮眉目传情 却不太想与人说话 我愿意 在清晨 听小鸟在枝头歌唱 却不太想与人说话 我愿意 在林...
    吴森迪阅读 169评论 0 0