iOS 一次性解决导航栏的所有问题

来源:互联网 发布:方腊手下大将排名 知乎 编辑:程序博客网 时间:2024/04/30 02:54

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

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


导航栏默认返回按钮

导航栏左上角的返回按钮,其文本默认为上一个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];;//创建UIBarButtonSystemItemFixedSpaceUIBarButtonItem * spaceItem = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];//将宽度设为负值spaceItem.width = -15;//将两个BarButtonItem都返回给NavigationItemself.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:中设置导航栏显示。
  2. 在每次Push前对当前页面进行截图并保存到数组,Pop时取数组最后一个元素显示,滑动结束后调用系统Pop方法并删除最后一张截图。
  3. 使用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都平移     // 挪动整个导航viewif (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,即窗口的根控制器的viewUIViewController *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];// 从上下文中,取出UIImageUIImage *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];}
0 0
原创粉丝点击