美文网首页iOS学习笔记 ios零碎记录常用技术收集
【iOS】让我们一次性解决导航栏的所有问题

【iOS】让我们一次性解决导航栏的所有问题

作者: _奔跑的炸鸡 | 来源:发表于2016-10-10 14:59 被阅读22522次

    更新:最后提供的所谓“终极”解决方案,之前都是自己的项目在用,分享出来之后,发现有一些地方还需要改进。但是总体思路不变,因此如非必要文章不会做大幅更改,最终代码请以文末github地址为准,另,欢迎提供Bug

    前言

    前一段时间换了工作,公司项目赶得比较紧,没有时间更新文章,现在闲下来了,赶紧写一篇来弥补自己的羞愧。
    今天我们来重点讨论导航栏返回的问题,包括各种问题的解决方案。


    系统默认导航栏的返回按钮和返回方式

    在默认情况下,导航栏返回按钮长这个样子

    导航栏默认返回按钮

    导航栏左上角的返回按钮,其文本默认为上一个ViewController的标题,如果上一个ViewController没有标题,则为Back(中文环境下为“返回”)。

    在默认情况下,导航栏返回的点击交互和滑动交互如下

    默认导航栏交互

    这些东西不需要任何设置和操作,因此也没有其他需要说明的地方。

    自定义左上角的返回按钮

    绝大多数情况下,我们都需要根据产品需求自定义左上角的返回按钮,虽然这对大多数开发者来说不是什么难事,但依然有几个问题值得注意。

    替换左上角返回按钮

    替换返回按钮非常简单,只需要在ViewController中创建一个UIBarButtonItem和一张图片,并为按钮添加相应的点击事件即可,代码如下
    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    UIButton * leftBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    leftBtn.frame = CGRectMake(0, 0, 25,25);
    [leftBtn setBackgroundImage:[UIImage imageNamed:@"nav_back"] forState:UIControlStateNormal];
    [leftBtn addTarget:self action:@selector(leftBarBtnClicked:) forControlEvents:UIControlEventTouchUpInside];
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithCustomView:leftBtn];
    }
    - (void)leftBarBtnClicked:(UIButton *)btn
    {
     [self.navigationController popViewControllerAnimated:YES];
    }
    

    我们来看一眼效果

    替换返回按钮
    调整按钮位置

    我们可以看到,上面的按钮是有点偏右的,那如果我们想调整按钮的位置该怎么做呢?设置Frame显然是行不通的,因为导航栏的NavigationItem是个比较特殊的View,我们无法通过简单的调整Frame来的调整左右按钮的位置。但是在苹果提供的UIButtonBarItem 中有个叫做UIBarButtonSystemItemFixedSpace的控件,利用它,我们就可以轻松调整返回按钮的位置。具体使用方法如下

    //创建返回按钮
    UIButton * leftBtn = [UIButton buttonWithType:UIButtonTypeSystem];
    leftBtn.frame = CGRectMake(0, 0, 25,25);
    [leftBtn setBackgroundImage:[UIImage imageNamed:@"icon_back"] forState:UIControlStateNormal];
    [leftBtn addTarget:self action:@selector(leftBarBtnClicked:) forControlEvents:UIControlEventTouchUpInside];
    UIBarButtonItem * leftBarBtn = [[UIBarButtonItem alloc]initWithCustomView:leftBtn];;
    //创建UIBarButtonSystemItemFixedSpace
    UIBarButtonItem * spaceItem = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
    //将宽度设为负值
    spaceItem.width = -15;
    //将两个BarButtonItem都返回给NavigationItem
    self.navigationItem.leftBarButtonItems = @[spaceItem,leftBarBtn];
    

    我们来看一眼效果

    调整返回按钮位置

    可以看到,我们的返回按钮已经紧靠着屏幕边缘。

    这个方法同样适用于调整导航栏右侧的按钮

    让滑动返回手势生效

    如果使用自定义的按钮去替换系统默认返回按钮,会出现滑动返回手势失效的情况。解决方法也很简单,只需要重新添加导航栏的interactivePopGestureRecognizerdelegate即可。
    首先为ViewContoller添加UIGestureRecognizerDelegate协议

    然后设置代理

    self.navigationController.interactivePopGestureRecognizer.delegate = self;
    

    至此,我们已经将返回按钮替换为我们的自定义按钮,并使滑动返回重新生效。接下来,我们继续来解决交互上的问题。

    全屏滑动返回

    这个一个很常见的需求,网上解决方案也很多,这里将本人常用的方法贴到这里。仅供参考
    实现全屏滑动返回仅需在导航栏给导航栏添加UIGestureRecognizerDelegate协议,并在ViewDidLoad中写入如下代码

    // 获取系统自带滑动手势的target对象
    id target = self.interactivePopGestureRecognizer.delegate;
    
    // 创建全屏滑动手势,调用系统自带滑动手势的target的action方法
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:@selector(handleNavigationTransition:)];
    
    // 设置手势代理,拦截手势触发
    pan.delegate = self;
    
    // 给导航控制器的view添加全屏滑动手势
    [self.view addGestureRecognizer:pan];
    
    // 禁止使用系统自带的滑动手势
    self.interactivePopGestureRecognizer.enabled = NO;
    

    我们来看一眼效果(注意鼠标位置)

    全屏滑动返回.gif

    成功

    这种方法的原理其实很简单,其实就是自定义一个全屏滑动手势,并将滑动事件设置为系统滑动事件,然后禁用系统滑动手势即可。handleNavigationTransition就是系统滑动的方法,虽然系统并未提供接口,但是我们我们可以通过runtime找到这个方法,因此直接调用即可。两位,不必担心什么私有API之类的问题,苹果如果按照方法名去判断是否使用私有API,那得误伤多少App。

    NavigationBar切换动画的“终极解决方案”

    本部分文字代码都较多,不想看这么多废话的同学请直接翻到末尾,文末附有下载地址,导入项目后,继承即可生效。

    在改变了导航栏样式,实现了全屏滑动返回之后,我们有了一个看起来还不错的导航栏。但是我们滑动时的切换依然是系统自带的动画,如果遇到前一个界面的NavigationBar为透明或前后两个Bar颜色不一样,这种渐变式的动画看起来就会不太友好,尤其当前后两个界面其中一个界面的NavigationBar为透明或隐藏时,其效果更是惨不忍睹。

    这个问题,其实很多App,比如天猫、美团等都通过一种“整体返回”的效果来解决这个问题。效果如下:

    整体滑动返回

    这种解决方案等于将两个NavigationBar独立开来,因此可以相对完美的解决导航栏滑动切换中的种种Bug。
    接下来,我们来看看如何实现这种效果。

    基本原理

    以我个人的认知,实现这个效果有三种基本思路:

    1. 使用UINavigationController自带的setNavigationBarHidden: animated:方法来实现,每次push或pop时,在当前控制器的viewWillDisappear:中设置隐藏,在要跳转的控制器的viewWillAppear:中设置导航栏显示。
    1. 在每次Push前对当前页面进行截图并保存到数组,Pop时取数组最后一个元素显示,滑动结束后调用系统Pop方法并删除最后一张截图。
    2. 使用iOS 7之后开放的,UIViewControllerAnimatedTransitioning协议,来实现自定义导航栏转场动画及交互。

    以上三种方法,方法一十分繁琐,而且会有很多莫名其妙的BUG,直接pass。

    在iOS的交互中,push一般通过按钮的点击事件或View的tap事件触发,而pop则可能通过事件触发,也可能通过右滑手势触发。因此,我们将这个我们要实现的动画效果分为交互效果和无交互效果两种,下面我们将使用方法2和方法3提供的思路,分别实现这两种效果,这样就能较为完美的解决Push和Pop的动画问题。

    实现交互动画效果

    准备需要使用的数组及手势
    #define ScreenWidth [UIScreen mainScreen].bounds.size.width
    #define ScreenHeight [UIScreen mainScreen].bounds.size.height
    @interface LTNavigationController ()<UIGestureRecognizerDelegate>
    @property(strong,nonatomic)UIImageView * screenshotImgView;
    @property(strong,nonatomic)UIView * coverView;
    @property(strong,nonatomic)NSMutableArray * screenshotImgs;
    @property(strong,nonatomic)UIPanGestureRecognizer *panGestureRec;
    @end
    
    @implementation LTNavigationController
    
    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    // 1,创建Pan手势识别器,并绑定监听方法
    _panGestureRec = [[UIScreenEdgePanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRec:)];
    _panGestureRec.edges = UIRectEdgeLeft;
    // 为导航控制器的view添加Pan手势识别器
    [self.view addGestureRecognizer:_panGestureRec];
    
    // 2.创建截图的ImageView
    _screenshotImgView = [[UIImageView alloc] init];
    // app的frame是包括了状态栏高度的frame
    _screenshotImgView.frame = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
    
    
    // 3.创建截图上面的黑色半透明遮罩
    _coverView = [[UIView alloc] init];
    // 遮罩的frame就是截图的frame
    _coverView.frame = _screenshotImgView.frame;
    // 遮罩为黑色
    _coverView.backgroundColor = [UIColor blackColor];
    
    // 4.存放所有的截图数组初始化
    _screenshotImgs = [NSMutableArray array];
    }
    
    实现手势的相应事件
    // 响应手势的方法
    - (void)panGestureRec:(UIPanGestureRecognizer *)panGestureRec
    {
    
    // 如果当前显示的控制器已经是根控制器了,不需要做任何切换动画,直接返回
    if(self.visibleViewController == self.viewControllers[0]) return;
    // 判断pan手势的各个阶段
    switch (panGestureRec.state) {
        case UIGestureRecognizerStateBegan:
            // 开始拖拽阶段
            [self dragBegin];
            break;
            
        case UIGestureRecognizerStateEnded:
            // 结束拖拽阶段
            [self dragEnd];
            break;
            
        default:
            // 正在拖拽阶段
            [self dragging:panGestureRec];
            break;
    }
    }
    
    #pragma mark 开始拖动,添加图片和遮罩
    - (void)dragBegin
    {
    // 重点,每次开始Pan手势时,都要添加截图imageview 和 遮盖cover到window中
    [self.view.window insertSubview:_screenshotImgView atIndex:0];
    [self.view.window insertSubview:_coverView aboveSubview:_screenshotImgView];
    
    // 并且,让imgView显示截图数组中的最后(最新)一张截图
    _screenshotImgView.image = [_screenshotImgs lastObject];
    //_screenshotImgView.transform = CGAffineTransformMakeTranslation(ScreenWidth, 0);
    }
    
    // 默认的将要变透明的遮罩的初始透明度(全黑)
    #define kDefaultAlpha 0.6
    
    // 当拖动的距离,占了屏幕的总宽高的3/4时, 就让imageview完全显示,遮盖完全消失
    #define kTargetTranslateScale 0.75
    #pragma mark 正在拖动,动画效果的精髓,进行位移和透明度变化
    - (void)dragging:(UIPanGestureRecognizer *)pan
    {
    
    // 得到手指拖动的位移
    CGFloat offsetX = [pan translationInView:self.view].x;
    
    // 让整个view都平移     // 挪动整个导航view
    if (offsetX > 0) {
        self.view.transform = CGAffineTransformMakeTranslation(offsetX, 0);
      }
    
    
    // 计算目前手指拖动位移占屏幕总的宽高的比例,当这个比例达到3/4时, 就让imageview完全显示,遮盖完全消失
    double currentTranslateScaleX = offsetX/self.view.frame.size.width;
    
    if (offsetX < ScreenWidth) {
        
        _screenshotImgView.transform = CGAffineTransformMakeTranslation((offsetX - ScreenWidth) * 0.6, 0);
    }
    
    // 让遮盖透明度改变,直到减为0,让遮罩完全透明,默认的比例-(当前平衡比例/目标平衡比例)*默认的比例
    double alpha = kDefaultAlpha - (currentTranslateScaleX/kTargetTranslateScale) * kDefaultAlpha;
    _coverView.alpha = alpha;
    }
    
    #pragma mark 结束拖动,判断结束时拖动的距离作相应的处理,并将图片和遮罩从父控件上移除
    - (void)dragEnd
    {
    // 取出挪动的距离
    CGFloat translateX = self.view.transform.tx;
    // 取出宽度
    CGFloat width = self.view.frame.size.width;
    
    if (translateX <= 40) {
        // 如果手指移动的距离还不到屏幕的一半,往左边挪 (弹回)
        [UIView animateWithDuration:0.3 animations:^{
            // 重要~~让被右移的view弹回归位,只要清空transform即可办到
            self.view.transform = CGAffineTransformIdentity;
            // 让imageView大小恢复默认的translation
            _screenshotImgView.transform = CGAffineTransformMakeTranslation(-ScreenWidth, 0);
            // 让遮盖的透明度恢复默认的alpha 1.0
            _coverView.alpha = kDefaultAlpha;
        } completion:^(BOOL finished) {
            // 重要,动画完成之后,每次都要记得 移除两个view,下次开始拖动时,再添加进来
            [_screenshotImgView removeFromSuperview];
            [_coverView removeFromSuperview];
        }];
    } else {
        // 如果手指移动的距离还超过了屏幕的一半,往右边挪
        [UIView animateWithDuration:0.3 animations:^{
            // 让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform
            self.view.transform = CGAffineTransformMakeTranslation(width, 0);
            // 让imageView位移还原
            _screenshotImgView.transform = CGAffineTransformMakeTranslation(0, 0);
            // 让遮盖alpha变为0,变得完全透明
            _coverView.alpha = 0;
        } completion:^(BOOL finished) {
            // 重要~~让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform,不然下次再次开始drag时会出问题,因为view的transform没有归零
            self.view.transform = CGAffineTransformIdentity;
            // 移除两个view,下次开始拖动时,再加回来
            [_screenshotImgView removeFromSuperview];
            [_coverView removeFromSuperview];
            
            // 执行正常的Pop操作:移除栈顶控制器,让真正的前一个控制器成为导航控制器的栈顶控制器
            [self popViewControllerAnimated:NO];
        }];
    }
    

    }

    实现截图保存功能,并在Push前截图
    - (void)screenShot
    {
    // 将要被截图的view,即窗口的根控制器的view
    UIViewController *beyondVC = self.view.window.rootViewController;
    // 背景图片 总的大小
    CGSize size = beyondVC.view.frame.size;
    // 开启上下文,使用参数之后,截出来的是原图(YES  0.0 质量高)
    UIGraphicsBeginImageContextWithOptions(size, YES, 0.0);
    // 要裁剪的矩形范围
    CGRect rect = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
    //注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
    [beyondVC.view drawViewHierarchyInRect:rect  afterScreenUpdates:NO];
    // 从上下文中,取出UIImage
    UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
    // 添加截取好的图片到图片数组
    if (snapshot) {
        [_screenshotImgs addObject:snapshot];
    }
    // 千万记得,结束上下文(移除栈顶的基于当前位图的图形上下文)
    UIGraphicsEndImageContext();
    }
    - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
      //有在导航控制器里面有子控制器的时候才需要截图
    if (self.viewControllers.count >= 1) {
        // 调用自定义方法,使用上下文截图
        [self screenShot];
    }
    // 截图完毕之后,才调用父类的push方法
    [super pushViewController:viewController animated:YES];
    }
    
    重写常用的pop方法

    在一开始基本原理地方,我们说过pop时要删除最后一张截图,用来保证数组中的最后一张截图是上一个控制器,但是很多情况下我们可能调用的是导航栏的popToViewController: animated:方法或popToRootViewControllerAnimated:来返回,这种情况下,我们删除的可能就不是一张截图,因此我们需要分别重写这些Pop方法,去确定我们要删除多少张图片,代码如下
    - (UIViewController *)popViewControllerAnimated:(BOOL)animated
    {
    [_screenshotImgs removeLastObject];
    return [super popViewControllerAnimated:animated];
    }
    - (NSArray<UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
    for (NSInteger i = self.viewControllers.count - 1; i > 0; i--) {
    if (viewController == self.viewControllers[i]) {
    break;
    }
    [_screenshotImgs removeLastObject];
    }

    return [super popToViewController:viewController animated:animated];
    }
    - (NSArray<UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated
    {
    [_screenshotImgs removeAllObjects];
    return [super popToRootViewControllerAnimated:animated];
    }
    
    ※在指定的控制器屏蔽手势

    在上面代码中,我们使用的是侧滑手势,并将相应区域设置为屏幕左侧。
    之所以不用全屏滑动,是因为全屏滑动手势在有些时候会和其他手势冲突,如果冲突的是我们自定义的手势,自然好解决,但如果是系统手势,如TableView的左滑菜单操作,这个事情就很蛋疼的。
    但是如果必须要做全屏滑动手势的话,我们可以对代码稍作修改,某些控制器中屏蔽手势。

    首先给导航栏添加禁用名单数组并配置

    ...
    @property(nonatomic,copy)NSArray * forbiddenArray;
    ...
    - (void)viewDidLoad {
    [super viewDidLoad];
    //原来代码
    ...
      //将手势禁用,之后在Push时根据条件开启
     self.panGestureRec.enabled = enable
    //将需要禁用手势的控制器的类名加到这个数组
    self.forbiddenArray = @[@"SCViewController",@"ManageAddressViewController"];
    }
    
    - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
    
    //在指定控制器中禁用手势  解决滑动返回手势和某些手势冲突问题
    BOOL enable = YES;
    for (NSString * string in self.forbiddenArray) {
        NSString * className = NSStringFromClass([viewController class]);
        if ([string isEqualToString:className]) {
            enable = NO;
        }
    }
    self.panGestureRec.enabled = enable;
    
    //原有代码
    ...
    }
    - (UIViewController *)popViewControllerAnimated:(BOOL)animated
    {
    NSInteger count = self.viewControllers.count;
    NSString * className = nil;
    if (count >= 2) {
        className = NSStringFromClass([self.viewControllers[count -2] class]);
    }
    
    BOOL enable = YES;
    for (NSString * string in self.forbiddenArray) {
        if ([string isEqualToString:className]) {
            enable = NO;
        }
    }
    self.panGestureRec.enabled = enable;
    //原有代码
    ...
    
    return [super popViewControllerAnimated:animated];
    }
    

    到了这里,我们已经完成了交互式的切换动画,效果跟开头一样,就不再截图。接下来我们来解决另一个大Boss-非交互式动画

    实现非交互动画效果

    理论基础

    这里我们就要用到之前说的UIViewControllerAnimatedTransitioning来实现。限于篇幅,这里不再详细介绍这部分的基础知识,大家可以移步这两篇博客做一个初步的了解

    向 UINavigationController 的传统动画说”再见” — 自定义过场动画(一)
    iOS 7:自定义导航转场动画以及更多

    实现原理

    注:FromVC代表即将消失的视图控制器,ToVC表示将要展示的视图控制器

    我们要实现的效果:
    Push的时候,FromVC往左移动,ToVC从屏幕右侧出现跟随FromVC左移直至FromVC消失,此时ToVC刚好完整显示在屏幕上。
    Pop的时候,FromVC向右移动,ToVC从屏幕边缘出现跟随FromVC向右移动直至FromVC消失,此时ToVC刚好完整显示在屏幕上

    实现的时候,我们依然需要将Push和Pop分开讨论
    先说Pop
    1.和交互式动画一样,每次Push时对屏幕截屏并保存,Pop的再次截屏但不保存
    2.把Pop时截取的图片作为FromVC展示,把Push到这个界面时截取的图片作为ToVC展示
    3.并对两张图片做位移动画,动画结束后移除两张图片

    然后是Push
    1.Push时先对当前屏幕截屏。
    2.将截取的图片保存方便Pop回来时使用,并把这张图片作为这次Push的FromVC保存。
    3.获取当前导航栏控制器对象,调整其Transform属性中的位移参数作为ToVC展示
    4.对截图和导航栏做位移,动画结束后直接移除截屏图片

    为什么要对导航栏作位移?

    首先,在Push结束之前,我们是无法知道ToVC具体是什么样子,系统的截屏方法对于未加载出来的View是无能为力的,而UIView的 snapshotViewAfterScreenUpdates:方法又无法带着导航栏一起映射到一个新的View上,因此视觉效果很差。
    正好在Pop的时候,为了达到想要的动画效果,用来展示的两张图片都需要放到导航栏的View上,因此在Push的时候我们就直接将导航栏的View做一个放射变换,当然,这也就意味着,当我们Push的时候,截屏就不能再放到导航栏上,而是应该放到它的“更上一层“ -- UITabbarController的View上

    让我们撸一发代码

    根据上述实现原理,我们可以知道,我们的主要工作重点在于打造一个合适的动画控制器。更准确的说,我们需要实现的细节都在UIViewControllerAnimatedTransitioning中,由于之前解释的很详细,这里我直接贴上相应代码供参考

    -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
    
    
    UIImageView * screentImgView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight)];
    UIImage * screenImg = [self screenShot];
    screentImgView.image =screenImg;
    
    //取出fromViewController,fromView和toViewController,toView
    UIViewController * fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    //    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    UIViewController * toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    
    
    CGRect fromViewEndFrame = [transitionContext finalFrameForViewController:fromViewController];
    fromViewEndFrame.origin.x = ScreenWidth;
    CGRect fromViewStartFrame = fromViewEndFrame;
    CGRect toViewEndFrame = [transitionContext finalFrameForViewController:toViewController];
    CGRect toViewStartFrame = toViewEndFrame;
    
    
    
    UIView * containerView = [transitionContext containerView];
    
    if (self.navigationOperation == UINavigationControllerOperationPush) {
    
        
        [self.screenShotArray addObject:screenImg];
        //toViewStartFrame.origin.x += ScreenWidth;
        [containerView addSubview:toView];
        
        toView.frame = toViewStartFrame;
    
        UIView * nextVC = [[UIView alloc]initWithFrame:CGRectMake(ScreenWidth, 0, ScreenWidth, ScreenHeight)];
         //[nextVC addSubview:[toView snapshotViewAfterScreenUpdates:YES]];
    
        [self.navigationController.tabBarController.view insertSubview:screentImgView atIndex:0];
        
        //[self.navigationController.tabBarController.view addSubview:nextVC];
        nextVC.layer.shadowColor = [UIColor blackColor].CGColor;
        nextVC.layer.shadowOffset = CGSizeMake(-0.8, 0);
        nextVC.layer.shadowOpacity = 0.6;
    
        self.navigationController.view.transform = CGAffineTransformMakeTranslation(ScreenWidth, 0);
        
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            //toView.frame = toViewEndFrame;
        self.navigationController.view.transform = CGAffineTransformMakeTranslation(0, 0);
            screentImgView.center = CGPointMake(-ScreenWidth/2, ScreenHeight / 2);
            //nextVC.center = CGPointMake(ScreenWidth/2, ScreenHeight / 2);
            
            
        } completion:^(BOOL finished) {
    
            [nextVC removeFromSuperview];
            [screentImgView removeFromSuperview];
            [transitionContext completeTransition:YES];
        }];
         
    }
    if (self.navigationOperation == UINavigationControllerOperationPop) {
        
    
        
        fromViewStartFrame.origin.x = 0;
        [containerView addSubview:toView];
        //若removeCount大于0  则说明Pop了不止一个控制器
        if (_removeCount > 0) {
            for (NSInteger i = 0; i < _removeCount; i ++) {
                if (i == _removeCount - 1) {
                    //当删除到要跳转页面的截图时,不再删除,并将该截图作为ToVC的截图展示
                    lastVcImgView.image = [self.screenShotArray lastObject];
                    _removeCount = 0;
                    break;
                }
                else
                {
                    [self.screenShotArray removeLastObject];
                }
                
            }
        }
        else
        {
            lastVcImgView.image = [self.screenShotArray lastObject];
        }
        lastVcImgView.image = [self.screenShotArray lastObject];
        screentImgView.layer.shadowColor = [UIColor blackColor].CGColor;
        screentImgView.layer.shadowOffset = CGSizeMake(-0.8, 0);
        screentImgView.layer.shadowOpacity = 0.6;
        [self.navigationController.tabBarController.view addSubview:lastVcImgView];
        [self.navigationController.tabBarController.view addSubview:screentImgView];
        
       // fromView.frame = fromViewStartFrame;
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            
            screentImgView.center = CGPointMake(ScreenWidth * 3 / 2 , ScreenHeight / 2);
            lastVcImgView.center = CGPointMake(ScreenWidth/2, ScreenHeight/2);
            //fromView.frame = fromViewEndFrame;
            
        } completion:^(BOOL finished) {
            //[self.navigationController setNavigationBarHidden:NO];
            [lastVcImgView removeFromSuperview];
            [screentImgView removeFromSuperview];
            [self.screenShotArray removeLastObject];
            [transitionContext completeTransition:YES];
    
        }];
    
      }
    
    }
    - (void)removeLastScreenShot
    {
    [self.screenShotArray removeLastObject];
    }
    - (UIImage *)screenShot
    {
    // 将要被截图的view,即窗口的根控制器的view(必须不含状态栏,默认ios7中控制器是包含了状态栏的)
    UIViewController *beyondVC = self.navigationController.view.window.rootViewController;
    // 背景图片 总的大小
    CGSize size = beyondVC.view.frame.size;
    // 开启上下文,使用参数之后,截出来的是原图(YES  0.0 质量高)
    UIGraphicsBeginImageContextWithOptions(size, YES, 0.0);
    // 要裁剪的矩形范围
    CGRect rect = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
    //注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
    [beyondVC.view drawViewHierarchyInRect:rect  afterScreenUpdates:NO];
    // 从上下文中,取出UIImage
    UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
    
    // 千万记得,结束上下文(移除栈顶的基于当前位图的图形上下文)
    UIGraphicsEndImageContext();
    
    
    
    // 返回截取好的图片
    return snapshot;
    
    }
    

    注:removeLastScreenShot需要在使用滑动手势Pop后调用,用来清除动画控制器中保存的截图,否则当交互式和非交互式动画交替使用时,会出现截图混乱的问题。

    更新:

    在调用 popToViewController:(UIViewController *)viewController animated:(BOOL)animated一次Pop多个页面,或调用popToRootViewControllerAnimated直接回到跟控制器时,一样需要清除对应数量的截图,并且需要和导航栏配合操作。新的代码已提交github,文章里也已经更新动画控制器对应的部分,具体代码还是以GitHub为准。

    看看效果

    我们将动画持续时间调制两秒,观察一下效果

    完成效果.gif

    后记

    这篇文章开始于四个月之前,中间由于个人以及工作原因拖了又拖,终于在最近补完,逻辑混乱之处请见谅。


    制作完成的导航栏和动画控制器的下载地址
    导航栏和动画控制器下载地址
    使用方法:
    1.将这四个文件导入工程
    2.将需要动画的导航栏继承KLTNavigationController即可

    如果我的文章对您有帮助,请点赞或评论,谢谢!

    相关文章

      网友评论

      • 阳仔dynamics:请问一下为什么我设置了leftBarButtonItem之后,push的时候会先展示一下系统自己的返回按钮样式,然后才会换成我自定义的样式呢?
      • lsif的简书:你好,请问一下,这个UIBarButtonSystemItemFixedSpace 属性想要靠最左边,设置负值并没有反应是为什么呀?
      • 只因为趁年轻:楼主,你隐藏导航栏的方法是什么,我用
        - (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
        {
        if ([viewController isKindOfClass:[HomeVC class]] ) {
        [self.navigationController setNavigationBarHidden:YES animated:animated];
        }
        else{
        [self.navigationController setNavigationBarHidden:NO animated:animated];
        }
        }
        这个方法隐藏,push两次后 然后pop回来的时候 导航条又显示了,push一次的话没有问题。能问下是为什么吗?
        labi3285:顺便也解决一下titleView的问题呀~
      • e46a376f96de:学习大佬的操作:smile:
      • liu_____:解决侧滑返回失效:
        @interface SRTLoginBaseViewController ()<UIGestureRecognizerDelegate>
        @property (nonatomic,strong) id originDelegate;
        @EnD

        - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        self.originDelegate = self.navigationController.interactivePopGestureRecognizer.delegate;
        self.navigationController.interactivePopGestureRecognizer.delegate = self;
        }

        - (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];
        self.navigationController.interactivePopGestureRecognizer.delegate = self.originDelegate;
        [SVProgressHUD dismiss];
        }
      • 消失的北极熊:调整导航栏左侧返回按钮位置之后,点击区域很大,是图片的2倍是怎么回事呢?
      • 闲来读者:你好,为什么push会闪一下白屏
      • 蜿蜒花骨朵:完美解决:smile: ,抱大腿感觉真好
      • A_rcher34:博主您好,NavigationBar切换动画的“终极解决方案”,有没有swift版本的?
      • Beyond_JG:iOS 11导航栏定义leftBarButtonItem点击事件不响应
      • xiAo__Ju: 修改返回按钮图片
        navigationBar.backIndicatorTransitionMaskImage
        navigationBar.backIndicatorImage
        修改图片位置
        let item = UIBarButtonItem(image: image, style: .plain, target: target, action: action)
        item.setValue(UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 0), forKey: "_imageInsets")
      • 34f930b15d3f:学习了!
      • 这个昵称就很帅:问个小问题:如果页面上有弹框,这时候应该禁掉左滑返回的,demo好像没有支持这个吧?作者可以完善一下。
      • 阿斯顿卡卡:你这是每push一层就把上一层的截屏???
      • ZhengYaWei:类似你最后一个效果图的效果没必要这么麻烦 两个控制器的viewWillAppear:方法中都写上 setNavigationBarHidden: animated:方法, animated:后面的参数都要求设置为YES即可,没必要这么麻烦。 如果碰到第一个控制器显示导航栏,第二个控制器隐藏导航的情况,对控制器中的返回手势处理一下就可以,我也是最近才发现的。
      • 小白猿:全屏滑动返回这一节中,是要自顶一个一个导航栏吗
        _奔跑的炸鸡:@小白猿 不用,只需要继承父类就OK
        小白猿:自顶一个一个导航栏吗
      • 51a9120806cc:空数据时候
        这个个方法就崩了
        - (BOOL)dzn_canDisplay
        {
        if (self.emptyDataSetSource && [self.emptyDataSetSource conformsToProtocol:@protocol(DZNEmptyDataSetSource)]) {
        if ([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]] || [self isKindOfClass:[UIScrollView class]]) {
        return YES;
        }
        }

        return NO;
        }
      • 从来吃不胖:讨厌一下子贴一堆代码的~:stuck_out_tongue:
      • 卟师:我能转载分享吗?我会标注上作者和出处的
        _奔跑的炸鸡:@卟师 没问题啊..
      • 89848af90932:太6了 正好学习下
      • 李乾坤David:这个文章太实用了,楼主棒!
      • 阿文灬:present出来的控制器,用不了你这种导航控制器
      • f1e24a8d40b9:那个jt第三方我也用过,很好用,截图也用过,不过后来发现还有一种最简单的方法,navigation试图的root试图实现导航代理,在导航代理的 即将切换下个试图的方法隐藏需要隐藏导航栏的类 就可以实现有bar和无bar的完美切换 bar不同自定义就可以了
        臭码农:@嘸___ 我之前也是跟你一样实现的,每一个根试图实现导航栏的代理,每个VC初始化时标记是否隐藏导航栏,默认显示,在根控制器每次pop和push时根据标记隐藏或显示,作者考虑到这种方式比较繁琐需要在很多地方实现
        xinnyu:@嘸___ 大神怎么玩
      • yuflying:很棒。学习了👍👍👍
      • sclcoder:导航栏的过渡,之前看过有人是这样做的,感觉这个思路不错~https://github.com/JNTian/JTNavigationController
        _奔跑的炸鸡:@sclcoder 恩,的确是一个很巧妙的思路
      • BlusSunShine:为何我在Push了一层页面之后 不会调用Pan的方法?
        _奔跑的炸鸡:@BlusSunShine pan 是加在导航栏上的,而且是pop 才会触发
        BlusSunShine:@_奔跑的炸鸡 侧滑返回。 断点在第一个VC划动会执行,在Push完成以后 第二个VC 的Pan就不会执行了。
        _奔跑的炸鸡:@BlusSunShine 你是使用的哪种方式? 滑动?还是点击左上角返回?
      • 玫瑰花瓣的信笺:可以写一篇关于抽屉,tabBar加导航的文章吗?抛析一下层次结构和具体的实现逻辑。最近想写一个类似QQ那种的抽屉,taBBar导航都有的界面死活写不成。。。发现这些累加到一起有各种问题,GitHub上下的引用,写着写着就会出各种问题,希望大神有空可以写一篇文章让我们新手学习学习。
        玫瑰花瓣的信笺:@_奔跑的炸鸡 嗯嗯,谢谢了。写好了麻烦给我个地址哈
        _奔跑的炸鸡:@玫瑰花瓣的信笺 可以,最近抽时间写一下吧
      • GOOGxu:只能说很好 不懂的人自然不会懂 谢谢
      • Doliant_H:写的非常详细,学习了~
      • 板砖程序猿: 照着楼主的文章 敲了代码 `全屏滑动返回` 这里 发现设置了按钮和代理 ,并不能滑动返回。 我去看下楼主你发的demo再试下吧。。
      • 充电星球:用截图的方法,那前面的页面就不能动态显示信内容了吧?
      • Rchongg:明天研究
      • StephenCurry300:全屏手势那个有个问题,pop到根viewController时,继续在该页面滑动,再点push会卡,你可以试下。
      • 风茗夜雨:push时视图左边的黑线很明显
        _奔跑的炸鸡:@怡红公子潇湘馆 那不是黑线... 那是我故意加的阴影.. 用来增加层次感
      • dongwenbo:厉害了
      • 苦笑男神:学习了
      • 西木柚子:如果手势操作失败就会出现侧滑卡主的情况,比如侧滑的时候又对页面进行点击,需要对animationController做如下修改:
        - (void)panGestureRec:(UIScreenEdgePanGestureRecognizer *)panGestureRec
        {
        if(self.visibleViewController == self.viewControllers[0]) return;
        switch (panGestureRec.state) {
        case UIGestureRecognizerStateBegan:
        [self dragBegin];
        break;

        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateFailed:
        case UIGestureRecognizerStateEnded:
        [self dragEnd];
        break;

        default:
        [self dragging:panGestureRec];
        break;
        }
        }

        使用6plus测试的时候发现侧滑出现过一次半屏的情况,重启app又无法复现
        _奔跑的炸鸡:@西木柚子 重写push方法,然后在push之前获取到要跳转的控制器的类名,然后根据这个类名判断,我文章里有写.. 非交互式的动画,只要在返回动画控制器的代理方法返回nil就行
        西木柚子:@_奔跑的炸鸡 请问,我想在某个VC禁用侧滑手势如何操作呢?使用之前禁用系统侧滑的方法发现无效
        _奔跑的炸鸡:@西木柚子 我会仔细测试,感谢提供
      • 雨三楼:隐藏系统导航栏自定义确实会有很多bug,而且实现起来需要很麻烦,尤其是导航栏title错乱的问题,简直无解……
        雨三楼:@回首凡尘不做闲 点赞 :smiley:
      • 孤居引:有个导航栏的问题请教,在调用系统相册的时候,怎么改变导航栏上的返回按钮?
      • soulDxl:Mark一下
      • noark9:终极的方法,还是自己做一个导航栏要方便点
      • mxdhqm:截图的方法不错,但是这样有个弊端,我前一个页面需要更新信息的话需要重新截图or?
        依然参考淘宝,跳转到第二个页面时,左侧滑动一半并没有完全pop出控制器时,第一个页面可以看到页面信息滚动
        _奔跑的炸鸡:@mxdhqm 按照目前这个解决方案,在Push的时候确实无法实时更新FromVC的内容
      • a43498affbed:代码即成进来有点异常,能否写一个完整的小demo,看一样效果。
        _奔跑的炸鸡:@sundy_武汉 你的项目是不是没有TabbarController,如果没有,就会出现这种情况,我在想办法改
        a43498affbed:@_奔跑的炸鸡 push的时候,有白色背景,然后在进入下一个页面,pop的时候没有动画。
        _奔跑的炸鸡:@sundy_武汉 具体是什么异常?
      • 大号鱼骨头:不错的思路,只使用系统的导航栏确实会出现一些蛋疼的bug,一般都会使用自定义view。以后可以尝试使用这种实现方式。
      • 萧城x:说得好
      • 善舞ice:不错,写的通俗易懂
      • 中秋梧桐语:为什么我左侧滑返回的时候,前一个控制器是白的,没看到截图。
      • eb8409d66aa1:之前在处理导航栏时,出现各种bug。很认真的读完了楼主的文章,很详细。点个赞 :+1:
      • LM333:不错不错,今天正好要解决返回按钮的问题,刚好看到你的文章,哈哈,很棒。
      • f953a9457b40:条条大路通罗马,楼主提供自己的思路和想法,很棒
      • 72fb329e4769:请问方法三呢。。。。方法三应该实现不了吧。你如果不使用自定义导航的话
        _奔跑的炸鸡:@_NAN_ 使用方法3其实最主要的目的是能够拿到导航栏Pop和Push时的动画过程,然后对这个过程加以改造,严格意义上来说,确实不算是单独用方法3实现
        72fb329e4769:@_奔跑的炸鸡 。。。。。你用这两个完成一个效果,应该不能算两种方法吧
        _奔跑的炸鸡:@_NAN_ 我后面实现非交互式动画用的不就是方法三吗..
      • firebirds:学习了
      • Azzan:很赞
      • a706aa61f4d8:简单实用
      • 三秒嗨:收藏~
      • 小明大神:非常有用,收藏了:smile:
      • nickName0:666,复杂的不如直接自定义:joy: 赞一个
        nickName0:@_奔跑的炸鸡 直接放一个View上去,把系统的隐藏掉
        _奔跑的炸鸡:@ZF00_Fly0 你想怎么自定义....
      • 我的儿子叫33:很厉害!
      • 不知晓:整体返回的第一种方法,隐藏和显示全部写在当前控制器就可以的
        Gavin_peng:@不知晓 那种方法,在从隐藏导航栏的控制器在Push到隐藏导航栏的控制器时会出现bug
      • ldldlkdldld:赞赞赞
      • nil_C:完美 !~
        _奔跑的炸鸡:@nil__ 谢谢 :blush:
      • angelen:作者很用心:grin::grin:
        _奔跑的炸鸡:@angelen 谢谢 :blush:
      • 昊囧:很棒,多谢了
        _奔跑的炸鸡:@昊囧 不客气 :smile:
      • jiangadam:整体返回没有那个复杂,试一下这个
        [self.navigationController setNavigationBarHidden:YES animated:animated];
        中秋梧桐语:@jiangadam 用这种方法实现的效果很差
        _奔跑的炸鸡:@jiangadam 而且模式十分固定,你无法自定义出更多的效果
        _奔跑的炸鸡:@jiangadam 我在文章中说的很明白了,这种方法引起的包括导航栏错乱等各种Bug,解决起来十分麻烦,而且通用型也不够强
      • zhouhao_180:最前面的”导航栏右上角的返回按钮,其文本默认为上一个ViewController的标题,如果上一个ViewController没有标题,则为Back(中文环境下为“返回”)。“应该是”左上角“吧
        _奔跑的炸鸡:@周浩zz 请原谅一个完全没有方向感的人。。。
        zhouhao_180:@_奔跑的炸鸡 “我们可以看到,上面的按钮是有点偏左的,那如果我们想调整按钮的位置该怎么做呢?”应该是偏右 :smiley:
        _奔跑的炸鸡:@周浩zz 额。。 是的,写的时候没注意,已更正
      • sunny冲哥:大赞,之前一直就在被自定义按钮后,滑动返回失效这个问题困扰.学习了
        _奔跑的炸鸡:@sunny落叶控 很高兴能帮到你 :smiley:
      • SaiWu:搞得有点复杂了,隐藏Navbar,自己写个顶部View解决所有问题。
        中秋梧桐语:@SaiWu 怎么写顶部View了 求教
        小收2222:@SaiWu 所有页面都隐藏自定义不会有问题,但是如果有的页面隐藏有的不隐藏,右滑返回时导航栏会有很多问题。
        _奔跑的炸鸡:@SaiWu 仁者见仁智者见智吧,这样做可以完整的展示前后页面的细节,在动画过程中导航栏的标题按钮颜色等都可以得到保留,个人认为值得这么做..
      • 巴图鲁:不错

      本文标题:【iOS】让我们一次性解决导航栏的所有问题

      本文链接:https://www.haomeiwen.com/subject/bwqelttx.html