彻底解决 iOS 导航栏样式控制问题

做 iOS 开发应该都知道,App 内导航栏样式控制一直是个问题,当 App 复杂起来后,每个界面的导航栏样式可能都不统一,包括背景颜色、是否隐藏导航栏、是否隐藏导航栏底部黑线等,如果前后两个界面样式不一致时,手势返回时(点击返回也会,只是太快不容易注意到),导航栏处就会出现明显的“断层”,极其丑陋,本文就是记录解决该问题,包含 Objective-C 和 Swift 5 的代码。

先看看效果对比

  • 优化前

  • 优化后

说明

解决问题的核心是自定义导航控制器转场动画,每次 push 新界面时会对上一个界面“截图”,并维护这个存储的“截图”队列,覆写常用的 navigate 界面方法进行对应的维护处理,与系统一样对滑动速度做了处理,不一定是划过的临界值触发。

Show me the code

Objective-C,两个类,四个文件,包括头文件

HDNavCAnimationController.h

//
//  HDNavCAnimationController.h
//  customer
//
//  Created by VanJay on 2019/6/6.
//  Copyright © 2018年 chaos network technology. All rights reserved.
//

#import <UIKit/UIKit.h>
/**
 导航栏操作类型
 */
#define ScreenWidth [UIScreen mainScreen].bounds.size.width
#define ScreenHeight [UIScreen mainScreen].bounds.size.height

@interface HDNavCAnimationController : NSObject <UIViewControllerAnimatedTransitioning>

+ (instancetype)animationControllerWithOperation:(UINavigationControllerOperation)operation;
+ (instancetype)animationControllerWithOperation:(UINavigationControllerOperation)operation navigationController:(UINavigationController *)navigationController;

@property (nonatomic, assign) UINavigationControllerOperation navigationOperation;
@property (nonatomic, weak) UINavigationController *navigationController;

/**
 导航栏Pop时删除了多少张截图(调用PopToViewController时,计算要删除的截图的数量)
 */
@property (nonatomic, assign) NSInteger removeCount;

/**
 调用此方法删除数组最后一张截图 (调用pop手势或一次pop多个控制器时使用)
 */
- (void)removeLastScreenShot;
/**
 移除全部屏幕截图
 */
- (void)removeAllScreenShot;
/**
 从截屏数组尾部移除指定数量的截图
 */
- (void)removeLastScreenShotWithNumber:(NSInteger)number;

/**
 移除指定索引的截图
 */
- (void)removeScreenShotAtIndex:(NSInteger)index;
@end

HDNavCAnimationController.m

//
//  HDNavCAnimationController.m
//  customer
//
//  Created by VanJay on 2019/6/6.
//  Copyright © 2018年 chaos network technology. All rights reserved.
//

#import "HDNavCAnimationController.h"

@interface HDNavCAnimationController ()

@property (nonatomic, strong) NSMutableArray *screenShotArray;
/**
 所属的导航栏有没有TabBarController
 */
@property (nonatomic, assign) BOOL isTabbarExist;

@end

@implementation HDNavCAnimationController

+ (instancetype)animationControllerWithOperation:(UINavigationControllerOperation)operation navigationController:(UINavigationController *)navigationController {
    HDNavCAnimationController *ac = [[HDNavCAnimationController alloc] init];
    ac.navigationController = navigationController;
    ac.navigationOperation = operation;
    return ac;
}

+ (instancetype)animationControllerWithOperation:(UINavigationControllerOperation)operation {
    HDNavCAnimationController *ac = [[HDNavCAnimationController alloc] init];
    ac.navigationOperation = operation;
    return ac;
}

- (void)setNavigationController:(UINavigationController *)navigationController {
    _navigationController = navigationController;

    UIViewController *beyondVC = self.navigationController.view.window.rootViewController;
    //判断该导航栏是否有TabBarController
    if (self.navigationController.tabBarController == beyondVC) {
        _isTabbarExist = YES;
    } else {
        _isTabbarExist = NO;
    }
}

- (NSMutableArray *)screenShotArray {
    if (!_screenShotArray) {
        _screenShotArray = [[NSMutableArray alloc] init];
    }
    return _screenShotArray;
}

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return .25f;
}

- (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];
    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];

        [containerView addSubview:toView];

        toView.frame = toViewStartFrame;

        UIView *nextVC = [[UIView alloc] initWithFrame:CGRectMake(ScreenWidth, 0, ScreenWidth, ScreenHeight)];

        //将截图添加到导航栏的View所属的window上
        [self.navigationController.view.window insertSubview:screentImgView atIndex:0];

        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:^{
                self.navigationController.view.transform = CGAffineTransformMakeTranslation(0, 0);
                screentImgView.center = CGPointMake(-ScreenWidth * 0.5, ScreenHeight * 0.5);
            }
            completion:^(BOOL finished) {
                [nextVC removeFromSuperview];
                [screentImgView removeFromSuperview];
                [transitionContext completeTransition:YES];
            }];
    } else if (self.navigationOperation == UINavigationControllerOperationPop) {
        fromViewStartFrame.origin.x = 0;
        [containerView addSubview:toView];

        UIImageView *lastVcImgView = [[UIImageView alloc] initWithFrame:CGRectMake(-ScreenWidth, 0, ScreenWidth, ScreenHeight)];
        //若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];
        }
        screentImgView.layer.shadowColor = [UIColor blackColor].CGColor;
        screentImgView.layer.shadowOffset = CGSizeMake(-0.8, 0);
        screentImgView.layer.shadowOpacity = 0.6;
        [self.navigationController.view.window addSubview:lastVcImgView];
        [self.navigationController.view addSubview:screentImgView];

        [UIView animateWithDuration:[self transitionDuration:transitionContext]
            animations:^{
                screentImgView.center = CGPointMake(ScreenWidth * 3 * 0.5, ScreenHeight * 0.5);
                lastVcImgView.center = CGPointMake(ScreenWidth * 0.5, ScreenHeight * 0.5);
            }
            completion:^(BOOL finished) {
                //[self.navigationController setNavigationBarHidden:NO];
                [lastVcImgView removeFromSuperview];
                [screentImgView removeFromSuperview];
                [self.screenShotArray removeLastObject];
                [transitionContext completeTransition:YES];
            }];
    }
}

- (void)removeLastScreenShot {
    [self.screenShotArray removeLastObject];
}

- (void)removeAllScreenShot {
    [self.screenShotArray removeAllObjects];
}

- (void)removeLastScreenShotWithNumber:(NSInteger)number {
    for (NSInteger i = 0; i < number; i++) {
        [self.screenShotArray removeLastObject];
    }
}

- (void)removeScreenShotAtIndex:(NSInteger)index {
    if (self.screenShotArray.count - 1 >= index) {
        [self.screenShotArray removeObjectAtIndex:index];
    }
}

- (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:替代

    //判读是导航栏是否有上层的Tabbar  决定截图的对象
    if (_isTabbarExist) {
        [beyondVC.view drawViewHierarchyInRect:rect afterScreenUpdates:NO];
    } else {
        [self.navigationController.view drawViewHierarchyInRect:rect afterScreenUpdates:NO];
    }
    // 从上下文中,取出UIImage
    UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();

    // 结束上下文(移除栈顶的基于当前位图的图形上下文)
    UIGraphicsEndImageContext();

    // 返回截取好的图片
    return snapshot;
}
@end

HDNavigationController.h


//
//  HDNavigationController.h
//  customer
//
//  Created by VanJay on 2019/6/6.
//  Copyright © 2018年 chaos network technology. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface HDNavigationController : UINavigationController

@property (nonatomic, strong) UIScreenEdgePanGestureRecognizer *panGestureRec;

@end

HDNavigationController.m

//
//  HDNavigationController.m
//  customer
//
//  Created by VanJay on 2019/6/6.
//  Copyright © 2018年 chaos network technology. All rights reserved.
//

#import "HDNavigationController.h"
#import "HDNavCAnimationController.h"
#import "UtilMacro.h"

// 临界滚动速度
static float const limitDraggingSpeed = 800.f;

#define ColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16)) / 255.0 green:((float)((rgbValue & 0xFF00) >> 8)) / 255.0 blue:((float)(rgbValue & 0xFF)) / 255.0 alpha:1.0]

// 默认的将要变透明的遮罩的初始透明度(全黑)
#define kDefaultAlpha 0.6

// 当拖动的距离,占了屏幕的总宽高的3/4时, 就让imageview完全显示,遮盖完全消失
#define kTargetTranslateScale 0.75

@interface HDNavigationController () <UIGestureRecognizerDelegate, UINavigationControllerDelegate>

@property (nonatomic, strong) UIImageView *screenshotImgView;
@property (nonatomic, strong) UIView *coverView;
@property (nonatomic, strong) NSMutableArray *screenshotImgs;
@property (nonatomic, strong) UIImage *nextVCScreenShotImg;
@property (nonatomic, strong) HDNavCAnimationController *animationController;

@property (nonatomic, assign) CFAbsoluteTime beginDraggingTime;  ///< 记录用户开始拖动的时间
@property (nonatomic, assign) CFAbsoluteTime endDraggingTime;    ///< 记录用户结束拖动的时间
@property (nonatomic, assign) CGFloat beginDraggingOffsetX;      ///< 记录用户开始拖动的偏移量X值
@property (nonatomic, assign) CGFloat endDraggingOffsetX;        ///< 记录用户结束拖动的偏移量X值

@end

@implementation HDNavigationController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.delegate = self;

    self.navigationBar.tintColor = ColorFromRGB(0x6F7179);

    self.view.layer.shadowColor = [UIColor blackColor].CGColor;
    self.view.layer.shadowOffset = CGSizeMake(-0.8, 0);
    self.view.layer.shadowOpacity = 0.6;

    // 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];
}

- (nullable id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                           animationControllerForOperation:(UINavigationControllerOperation)operation
                                                        fromViewController:(UIViewController *)fromVC
                                                          toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0) {
    self.animationController.navigationOperation = operation;
    self.animationController.navigationController = self;
    return self.animationController;
}

- (HDNavCAnimationController *)animationController {
    if (!_animationController) {
        _animationController = [[HDNavCAnimationController alloc] init];
    }
    return _animationController;
}

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {

    // 只有在导航控制器里面有子控制器的时候才需要截图
    if (self.viewControllers.count >= 1) {
        // 调用自定义方法,使用上下文截图
        [self screenShot];
    }

    [super pushViewController:viewController animated:animated];
}

- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
    NSInteger index = self.viewControllers.count;
    NSString *className = nil;
    if (index >= 2) {
        className = NSStringFromClass(self.viewControllers[index - 2].class);
    }

    if (_screenshotImgs.count >= index - 1) {
        [_screenshotImgs removeLastObject];
    }

    return [super popViewControllerAnimated:animated];
}

- (NSArray<UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated {

    NSInteger removeCount = 0;
    for (NSInteger i = self.viewControllers.count - 1; i > 0; i--) {
        if (viewController == self.viewControllers[i]) {
            break;
        }

        [_screenshotImgs removeLastObject];
        removeCount++;
    }
    _animationController.removeCount = removeCount;
    return [super popToViewController:viewController animated:animated];
}

- (NSArray<UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated {
    [_screenshotImgs removeAllObjects];
    [_animationController removeAllScreenShot];
    return [super popToRootViewControllerAnimated:animated];
}

- (void)setViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers {
    // 获取当前 viewControllers
    NSArray<__kindof UIViewController *> *currentViewControllers = self.viewControllers;
    if (viewControllers.count != currentViewControllers.count) {  // 数量不一致,获取差异控制器,得到索引,移除截图
        for (UIViewController *oldVC in currentViewControllers) {
            if (![viewControllers containsObject:oldVC]) {
                NSInteger index = [currentViewControllers indexOfObject:oldVC];
                if (_screenshotImgs.count - 1 >= index) {
                    [_screenshotImgs removeObjectAtIndex:index];
                }
                [_animationController removeScreenShotAtIndex:index];
            }
        }
    }
    [super setViewControllers:viewControllers];
}

// 使用上下文截图,并使用指定的区域裁剪,模板代码
- (void)screenShot {
    // 将要被截图的view,即窗口的根控制器的view(必须不含状态栏,默认ios7中控制器是包含了状态栏的)
    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:替代
    //判读是导航栏是否有上层的Tabbar  决定截图的对象
    if (self.tabBarController == beyondVC) {
        [beyondVC.view drawViewHierarchyInRect:rect afterScreenUpdates:NO];
    } else {
        [self.view drawViewHierarchyInRect:rect afterScreenUpdates:NO];
    }
    // 从上下文中,取出UIImage
    UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
    // 添加截取好的图片到图片数组
    if (snapshot) {
        [_screenshotImgs addObject:snapshot];
    }

    // 结束上下文(移除栈顶的基于当前位图的图形上下文)
    UIGraphicsEndImageContext();
}

// 监听手势的方法,只要是有手势就会执行
- (void)panGestureRec:(UIScreenEdgePanGestureRecognizer *)panGestureRec {

    // 如果当前显示的控制器已经是根控制器了,不需要做任何切换动画,直接返回
    if (self.visibleViewController == self.viewControllers[0]) return;

    // 判断pan手势的各个阶段
    switch (panGestureRec.state) {
        case UIGestureRecognizerStateBegan:
            // 开始拖拽阶段
            [self dragBegin];
            break;
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateFailed:
        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];

    UIViewController *lastVC = self.viewControllers.lastObject;
    if (lastVC) {
        // 隐藏键盘
        [lastVC.view endEditing:YES];
    }

    // 记录用户开始拖动的时间
    self.beginDraggingTime = CFDateGetAbsoluteTime((CFDateRef)[NSDate date]);

    // 记录用户开始拖动的偏移量Y值
    self.beginDraggingOffsetX = 0;
}

#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;

    // 记录用户结束拖动的时间
    self.endDraggingTime = CFDateGetAbsoluteTime((CFDateRef)[NSDate date]);

    // 记录用户结束拖动的偏移量Y值
    self.endDraggingOffsetX = translateX;

    // 计算得到间隔时间
    CFAbsoluteTime minusTime = self.endDraggingTime - self.beginDraggingTime;
    // 间隔offset
    CGFloat minusOffset = self.endDraggingOffsetX - self.beginDraggingOffsetX;

    float draggingSpeed = ABS(minusOffset) / minusTime;

    HDLog(@"滑动速度:%.0f", draggingSpeed);

    if (draggingSpeed > limitDraggingSpeed || translateX > ScreenWidth * 0.5) {
        // 如果手指移动的距离超过了屏幕的一半,或者滑动速度达到临界值,往右边挪
        [UIView animateWithDuration:0.25
            animations:^{
                // 让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform
                self.view.transform = CGAffineTransformMakeTranslation(width, 0);
                // 让imageView位移还原
                self.screenshotImgView.transform = CGAffineTransformMakeTranslation(0, 0);
                // 让遮盖alpha变为0,变得完全透明
                self.coverView.alpha = 0;
            }
            completion:^(BOOL finished) {
                // 重要~~让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform,不然下次再次开始drag时会出问题,因为view的transform没有归零
                self.view.transform = CGAffineTransformIdentity;
                // 移除两个view,下次开始拖动时,再加回来
                [self.screenshotImgView removeFromSuperview];
                [self.coverView removeFromSuperview];

                // 执行正常的Pop操作:移除栈顶控制器,让真正的前一个控制器成为导航控制器的栈顶控制器
                [self popViewControllerAnimated:NO];

                // 重要~记得这时候,可以移除截图数组里面最后一张没用的截图了
                [self.animationController removeLastScreenShot];
            }];
    } else {
        // 如果手指移动的距离还不到屏幕的一半, 或者滑动速度未达到临界值,恢复原样
        [UIView animateWithDuration:0.25
            animations:^{
                // 重要~~让被右移的view弹回归位,只要清空transform即可办到
                self.view.transform = CGAffineTransformIdentity;
                // 让imageView大小恢复默认的translation
                self.screenshotImgView.transform = CGAffineTransformMakeTranslation(-ScreenWidth, 0);
                // 让遮盖的透明度恢复默认的alpha 1.0
                self.coverView.alpha = kDefaultAlpha;
            }
            completion:^(BOOL finished) {
                // 重要,动画完成之后,每次都要记得 移除两个view,下次开始拖动时,再添加进来
                [self.screenshotImgView removeFromSuperview];
                [self.coverView removeFromSuperview];
            }];
    }
}
@end

Swift 5,两个文件

NavigationTransition.swift

//
//  NavigationTransition.swift
//  ViPay
//
//  Created by VanJay on 2019/6/30.
//  Copyright © 2019 VanJay. All rights reserved.
//

import UIKit

class NavigationTransition: NSObject {
    var navigationOperation: UINavigationController.Operation!
    weak var navigationController: UINavigationController? {
        willSet {
            let rootViewController = navigationController?.view.window?.rootViewController

            if rootViewController == navigationController?.tabBarController {
                isTabbarExist = true
            } else {
                isTabbarExist = false
            }
        }
    }

    /// 导航栏Pop时删除了多少张截图(调用PopToViewController时,计算要删除的截图的数量)
    var removeCount = 0

    private lazy var screenShotArray = [UIImage]()
    /// 所属的导航栏有没有TabBarController
    private var isTabbarExist: Bool = false

    convenience init(operation: UINavigationController.Operation) {
        self.init()
        navigationOperation = operation
    }

    convenience init(operation: UINavigationController.Operation, navigationController: UINavigationController?) {
        self.init()
        navigationOperation = operation
        self.navigationController = navigationController
    }

    // MARK: - public methods

    /// 调用此方法删除数组最后一张截图 (调用pop手势或一次pop多个控制器时使用)
    func removeLastScreenShot() {
        screenShotArray.removeLast()
    }

    /// 移除全部屏幕截图
    func removeAllScreenShot() {
        screenShotArray.removeAll()
    }

    /// 从截屏数组尾部移除指定数量的截图
    func removeLastScreenShot(withNumber number: Int) {
        for _ in 0 ..< number {
            screenShotArray.removeLast()
        }
    }

    /// 移除指定索引的截图
    func removeScreenShot(at index: Int) {
        guard screenShotArray.count - 1 >= index else {
            return
        }
        screenShotArray.remove(at: index)
    }
}

extension NavigationTransition: UIViewControllerAnimatedTransitioning {
    // MARK: - UIViewControllerAnimatedTransitioning

    func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.25
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let screentImgView = UIImageView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: UIScreen.height))
        let screenImg = screenShot()
        screentImgView.image = screenImg

        // 取出fromViewController,fromView和toViewController,toView
        let fromViewController = (transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from))!

        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)

        var fromViewEndFrame = transitionContext.finalFrame(for: fromViewController)
        fromViewEndFrame.origin.x = UIScreen.width
        var fromViewStartFrame = fromViewEndFrame
        let toViewEndFrame = transitionContext.finalFrame(for: toViewController!)
        let toViewStartFrame = toViewEndFrame

        let containerView = transitionContext.containerView

        if navigationOperation == UINavigationController.Operation.push {
            screenShotArray.append(screenImg)
            containerView.addSubview(toView!)

            toView?.frame = toViewStartFrame

            let nextVC = UIView(frame: CGRect(x: UIScreen.width, y: 0, width: UIScreen.width, height: UIScreen.height))

            // 将截图添加到导航栏的View所属的window上
            navigationController?.view.window?.insertSubview(screentImgView, at: 0)
            nextVC.layer.shadowColor = UIColor.black.cgColor
            nextVC.layer.shadowOffset = CGSize(width: -0.8, height: 0)
            nextVC.layer.shadowOpacity = 0.6
            navigationController?.view.transform = CGAffineTransform(translationX: UIScreen.width, y: 0)

            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                self.navigationController?.view.transform = CGAffineTransform(translationX: 0, y: 0)
                screentImgView.center = CGPoint(x: -UIScreen.width * 0.5, y: UIScreen.height * 0.5)
            }) { _ in
                nextVC.removeFromSuperview()
                screentImgView.removeFromSuperview()
                transitionContext.completeTransition(true)
            }
        } else if navigationOperation == UINavigationController.Operation.pop {
            fromViewStartFrame.origin.x = 0
            containerView.addSubview(toView!)

            let lastVcImgView = UIImageView(frame: CGRect(x: UIScreen.width, y: 0, width: UIScreen.width, height: UIScreen.height))
            // 若removeCount大于0  则说明Pop了不止一个控制器
            if removeCount > 0 {
                for i in 0 ..< removeCount {
                    if i == removeCount - 1 {
                        // 当删除到要跳转页面的截图时,不再删除,并将该截图作为ToVC的截图展示
                        lastVcImgView.image = screenShotArray.last
                        removeCount = 0
                        break
                    } else {
                        screenShotArray.removeLast()
                    }
                }
            } else {
                lastVcImgView.image = screenShotArray.last
            }
            screentImgView.layer.shadowColor = UIColor.black.cgColor
            screentImgView.layer.shadowOffset = CGSize(width: -0.8, height: 0)
            screentImgView.layer.shadowOpacity = 0.6
            navigationController?.view.window?.addSubview(lastVcImgView)
            navigationController?.view.window?.addSubview(screentImgView)

            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                screentImgView.center = CGPoint(x: UIScreen.width * 3 * 0.5, y: UIScreen.height * 0.5)
                lastVcImgView.center = CGPoint(x: UIScreen.width * 0.5, y: UIScreen.height * 0.5)
            }) { _ in
                lastVcImgView.removeFromSuperview()
                screentImgView.removeFromSuperview()
                self.screenShotArray.removeLast()
                transitionContext.completeTransition(true)
            }
        }
    }

    func screenShot() -> UIImage {
        // 将要被截图的view,即窗口的根控制器的view(必须不含状态栏,默认ios7中控制器是包含了状态栏的)
        let rootViewController = navigationController?.view?.window?.rootViewController
        // 背景图片 总的大小
        let size = rootViewController!.view.frame.size
        // 开启上下文,使用参数之后,截出来的是原图(YES  0.0 质量高)
        UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
        // 要裁剪的矩形范围
        let rect = CGRect(x: 0, y: 0, width: UIScreen.width, height: UIScreen.height)
        // 注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代

        // 判读是导航栏是否有上层的Tabbar  决定截图的对象
        if isTabbarExist {
            rootViewController?.view.drawHierarchy(in: rect, afterScreenUpdates: false)
        } else {
            navigationController?.view.drawHierarchy(in: rect, afterScreenUpdates: false)
        }
        // 从上下文中,取出UIImage
        let snapshot = UIGraphicsGetImageFromCurrentImageContext()

        // 结束上下文(移除栈顶的基于当前位图的图形上下文)
        UIGraphicsEndImageContext()

        // 返回截取好的图片
        return snapshot!
    }
}

NavigationController.swift

//
//  NavigationController.swift
//  ViPay
//
//  Created by VanJay on 2019/6/30.
//  Copyright © 2019 VanJay. All rights reserved.
//

import UIKit

class NavigationController: UINavigationController {
    /// 临界滚动速度
    private let limitDraggingSpeed: CGFloat = 800.0
    /// 默认的将要变透明的遮罩的初始透明度(全黑)
    private let kDefaultAlpha: CGFloat = 0.6
    /// 当拖动的距离,占了屏幕的总宽高的 3/4 时, 就让imageview完全显示,遮盖完全消失
    private let kTargetTranslateScale: CGFloat = 0.75
    private var panGestureRec: UIScreenEdgePanGestureRecognizer!
    private var screenshotImgView: UIImageView!
    private var coverView: UIView!
    private var screenshotImgs: [UIImage]!
    private var nextVCScreenShotImg: UIImage?
    private lazy var animationController: NavigationTransition = {
        NavigationTransition()
    }()

    /// 记录用户开始拖动的时间
    private var beginDraggingTime: TimeInterval = 0.0
    /// 记录用户结束拖动的时间
    private var endDraggingTime: TimeInterval = 0.0
    /// 记录用户开始拖动的偏移量X值
    private var beginDraggingOffsetX: CGFloat = 0.0
    /// 记录用户结束拖动的偏移量X值
    private var endDraggingOffsetX: CGFloat = 0.0

    override func viewDidLoad() {
        super.viewDidLoad()

        delegate = self

        navigationBar.tintColor = UIColor(hexString: "#6F7179")

        view.layer.shadowColor = UIColor.black.cgColor
        view.layer.shadowOffset = CGSize(width: -0.8, height: 0)
        view.layer.shadowOpacity = 0.6

        // 1,创建Pan手势识别器,并绑定监听方法
        let panGestureRec = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(screenEdgeSwiped))
        panGestureRec.edges = .left
        // 为导航控制器的view添加Pan手势识别器
        view.addGestureRecognizer(panGestureRec)
        self.panGestureRec = panGestureRec

        // 2.创建截图的ImageView
        screenshotImgView = UIImageView()
        // app的frame是包括了状态栏高度的frame
        screenshotImgView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: UIScreen.height)

        // 3.创建截图上面的黑色半透明遮罩
        coverView = UIView()
        // 遮罩的frame就是截图的frame
        coverView.frame = screenshotImgView.frame
        // 遮罩为黑色
        coverView.backgroundColor = .black

        // 4.存放所有的截图数组初始化
        screenshotImgs = [UIImage]()
    }

    // MARK: - event

    @objc func screenEdgeSwiped(_ recognizer: UIScreenEdgePanGestureRecognizer) {
        // 如果当前显示的控制器已经是根控制器了,不需要做任何切换动画,直接返回
        guard visibleViewController != viewControllers[0]
        else {
            return
        }

        // 判断pan手势的各个阶段
        switch panGestureRec.state {
        case .began:
            // 开始拖拽阶段
            dragBegin()
        case .cancelled, .failed, .ended:
            // 结束拖拽阶段
            dragEnd()

        default:
            // 正在拖拽阶段
            dragging(recognizer: recognizer)
        }
    }

    // MARK: - override

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        // 只有在导航控制器里面有子控制器的时候才需要截图
        if viewControllers.count >= 1 {
            // 调用自定义方法,使用上下文截图
            screenShot()
        }
        super.pushViewController(viewController, animated: animated)
    }

    override func popViewController(animated: Bool) -> UIViewController? {
        let index = viewControllers.count

        if screenshotImgs.count >= index - 1 {
            screenshotImgs.removeLast()
        }

        return super.popViewController(animated: animated)
    }

    override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
        var removeCount = 0
        var count = viewControllers.count - 1
        while count > 0 {
            if viewController == viewControllers[count] {
                break
            }

            count = count - 1
            removeCount = removeCount + 1
        }
        animationController.removeCount = removeCount
        return super.popToViewController(viewController, animated: animated)
    }

    override func popToRootViewController(animated: Bool) -> [UIViewController]? {
        screenshotImgs.removeAll()
        animationController.removeAllScreenShot()
        return super.popToRootViewController(animated: animated)
    }

    override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
        // 获取当前 viewControllers
        let currentViewControllers = self.viewControllers
        if viewControllers.count != currentViewControllers.count { // 数量不一致,获取差异控制器,得到索引,移除截图
            for case let oldVC in currentViewControllers as [UIViewController] {
                if !viewControllers.contains(oldVC) {
                    if let index = currentViewControllers.firstIndex(of: oldVC) {
                        if screenshotImgs.count - 1 >= index {
                            screenshotImgs.remove(at: index)
                        }
                        animationController.removeScreenShot(at: index)
                    }
                }
            }
        }
        super.setViewControllers(viewControllers, animated: animated)
    }

    // MARK: - provate methods

    /// 拖动开始
    private func dragBegin() {
        // 重点,每次开始Pan手势时,都要添加截图imageview 和 遮盖cover到window中
        view.window?.insertSubview(screenshotImgView, at: 0)
        view.window?.insertSubview(coverView, aboveSubview: screenshotImgView)

        // 并且,让imgView显示截图数组中的最后(最新)一张截图
        screenshotImgView.image = screenshotImgs.last

        if let lastVC = self.viewControllers.last {
            // 隐藏键盘
            lastVC.view.endEditing(true)
        }

        // 记录用户开始拖动的时间
        beginDraggingTime = Date().timeIntervalSince1970

        // 记录用户开始拖动的偏移量Y值
        beginDraggingOffsetX = 0
    }

    /// 拖动结束
    private func dragEnd() {
        // 取出挪动的距离
        let translateX = view.transform.tx
        // 取出宽度
        let width = view.frame.size.width

        // 记录用户结束拖动的时间
        endDraggingTime = Date().timeIntervalSince1970

        // 记录用户结束拖动的偏移量Y值
        endDraggingOffsetX = translateX

        // 计算得到间隔时间
        let minusTime = endDraggingTime - beginDraggingTime
        // 间隔offset
        let minusOffset = endDraggingOffsetX - beginDraggingOffsetX

        let draggingSpeed = abs(CGFloat(minusOffset)) / CGFloat(minusTime)

        printLog("滑动速度:\(draggingSpeed)")

        if draggingSpeed > limitDraggingSpeed || translateX > UIScreen.width * 0.5 {
            // 如果手指移动的距离超过了屏幕的一半,或者滑动速度达到临界值,往右边挪
            UIView.animate(withDuration: 0.25, animations: {
                // 让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform
                self.view.transform = CGAffineTransform(translationX: width, y: 0)
                // 让imageView位移还原
                self.screenshotImgView.transform = CGAffineTransform(translationX: 0, y: 0)
                // 让遮盖alpha变为0,变得完全透明
                self.coverView.alpha = 0
            }) { _ in
                // 重要~~让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform,不然下次再次开始drag时会出问题,因为view的transform没有归零
                self.view.transform = CGAffineTransform.identity
                // 移除两个view,下次开始拖动时,再加回来
                self.screenshotImgView.removeFromSuperview()
                self.coverView.removeFromSuperview()

                // 执行正常的Pop操作:移除栈顶控制器,让真正的前一个控制器成为导航控制器的栈顶控制器
                _ = self.popViewController(animated: false)

                // 重要~记得这时候,可以移除截图数组里面最后一张没用的截图了
                self.animationController.removeLastScreenShot()
            }
        } else {
            // 如果手指移动的距离还不到屏幕的一半, 或者滑动速度未达到临界值,恢复原样
            UIView.animate(withDuration: 0.25, animations: {
                // 重要~~让被右移的view弹回归位,只要清空transform即可办到
                self.view.transform = CGAffineTransform.identity
                // 让imageView大小恢复默认的translation
                self.screenshotImgView.transform = CGAffineTransform(translationX: -UIScreen.width, y: 0)
                // 让遮盖的透明度恢复默认的alpha 1.0
                self.coverView.alpha = self.kDefaultAlpha
            }) { _ in
                // 重要,动画完成之后,每次都要记得 移除两个view,下次开始拖动时,再添加进来
                self.screenshotImgView.removeFromSuperview()
                self.coverView.removeFromSuperview()
            }
        }
    }

    /// 正在拖动
    private func dragging(recognizer: UIScreenEdgePanGestureRecognizer) {
        // 得到手指拖动的位移
        let offsetX = recognizer.translation(in: view).x

        // 让整个view都平移
        // 挪动整个导航view
        if offsetX > 0 {
            view.transform = CGAffineTransform(translationX: offsetX, y: 0)
        }

        // 计算目前手指拖动位移占屏幕总的宽高的比例,当这个比例达到3/4时, 就让imageview完全显示,遮盖完全消失
        let currentTranslateScaleX = offsetX / view.frame.size.width

        if offsetX < UIScreen.width {
            screenshotImgView.transform = CGAffineTransform(translationX: (offsetX - UIScreen.width) * 0.6, y: 0)
        }

        // 让遮盖透明度改变,直到减为0,让遮罩完全透明,默认的比例-(当前平衡比例/目标平衡比例)*默认的比例
        let alpha = kDefaultAlpha - currentTranslateScaleX / kTargetTranslateScale * kDefaultAlpha
        coverView.alpha = alpha
    }

    private func screenShot() {
        // 将要被截图的view,即窗口的根控制器的view(必须不含状态栏,默认ios7中控制器是包含了状态栏的)
        let rootViewController = view.window?.rootViewController
        // 背景图片 总的大小
        let size = rootViewController?.view.frame.size
        // 开启上下文,使用参数之后,截出来的是原图(YES  0.0 质量高)
        UIGraphicsBeginImageContextWithOptions(size!, true, 0.0)
        // 要裁剪的矩形范围
        let rect = CGRect(x: 0, y: 0, width: UIScreen.width, height: UIScreen.height)
        // 注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
        // 判读是导航栏是否有上层的Tabbar  决定截图的对象
        if tabBarController == rootViewController {
            rootViewController?.view.drawHierarchy(in: rect, afterScreenUpdates: false)
        } else {
            view.drawHierarchy(in: rect, afterScreenUpdates: false)
        }
        // 从上下文中,取出UIImage
        let snapshot = UIGraphicsGetImageFromCurrentImageContext()
        // 添加截取好的图片到图片数组
        if snapshot != nil {
            screenshotImgs.append(snapshot!)
        }

        // 结束上下文(移除栈顶的基于当前位图的图形上下文)
        UIGraphicsEndImageContext()
    }
}

extension NavigationController: UINavigationControllerDelegate {
    // MARK: - UINavigationControllerDelegate

    func navigationController(_: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from _: UIViewController, to _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        animationController.navigationOperation = operation
        animationController.navigationController = self
        return animationController
    }
}

使用

继承自导航控制器即可。可用 runtime 实现这套需求,但个人觉得没必要,对于开发,能正向常规完成的 ,尽量不使用逆向或者 hook 等方式,避免一些怪异 bug 的产生(一般不会)。