彻底解决 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 的产生(一般不会)。