你的位置:首页 > 操作系统

[操作系统]iOS 隔离导航控制器带来的坑


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

 

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

  这个名词纯粹是我自己瞎编的,我不知道有没有这种说法,至少我没有看到。我所说的隔离导航控制器是指:在导航栏样式不同的页面采用不同的导航控制器。现在同一个导航栏发生变化主要体现在:隐藏导航栏页面跳转到非隐藏导航栏的页面、A颜色导航栏的页面跳转到B颜色导航栏的页面、可跟随滑动做动画的页面跳转到固定导航栏的页面。隔离导航控制器就是:当页面跳转到导航栏不同的页面时不再使用同一个导航控制器,而是弹出一个新的导航控制器,这个导航控制器的导航栏固定不变,如果发生变化那么再弹出一个新的导航控制器。

 

二、为什么要隔离导航控制器?

  很简单,因为我已经受够了页面的来回跳转导致状态栏储存、变化、恢复这样的过程。尤其在侧滑或者全屏滑动返回时,导航栏的变化会有不好的体验。所以在很久以前我就在酝酿,如果present出来一个导航控制器就好了(隔离导航控制器并不是一个很好地解决办法,但是可以试一试,毕竟我很闲)。因此我在新的项目中试了一下,结果遇到了一些问题,自己挖的坑,跪着也要填满了。

 

三、实现右进右出的present

  ViewController 的present样式一共有4种:

typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {  UIModalTransitionStyleCoverVertical = 0,//默认  UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,//翻转  UIModalTransitionStyleCrossDissolve,//透明度渐变  UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,//翻书};

 

[vc setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];  //present 前加上这句就行[self presentViewController:vc animated:YES completion:nil];

 没有我们需要的效果,因此我们需要利用iOS 7 以后的转场动画来实现。说道转场动画不怕大家笑话,我已经看了好多遍了,但是依然记不住那些名字。每次要用都要去王巍的博客里再学习一边(https://onevcat.com/2013/10/vc-transition-in-ios7/)。我的代码也是使用王巍的代码,因为觉得没有必要再写一遍。只不过我做了略微的修改。

// NormalDismissAnimation // 4. Do animate now  NSTimeInterval duration = [self transitionDuration:transitionContext];  toVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是为了上下两个控制器有联动的效果  [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{    fromVC.view.frame = finalFrame;    toVC.view.transform = CGAffineTransformIdentity;  } completion:^(BOOL finished) {    [transitionContext completeTransition:![transitionContext transitionWasCancelled]];  }];//BouncePresentAnimation // 4. Do animate now  NSTimeInterval duration = [self transitionDuration:transitionContext];  [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{    toVC.view.frame = finalFrame;    fromVC.view.transform = CGAffineTransformMakeTranslation(-100, 0);//主要是为了有上下两个控制器联动的效果  } completion:^(BOOL finished) {    fromVC.view.transform = CGAffineTransformIdentity;    [transitionContext completeTransition:YES];  }];//SwipeUpInteractiveTransition- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {  CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];    switch (gestureRecognizer.state) {    case UIGestureRecognizerStateBegan:      // 1. Mark the interacting flag. Used when supplying it in delegate.      self.interacting = YES;      [self.presentingVC dismissViewControllerAnimated:YES completion:nil];      break;    case UIGestureRecognizerStateChanged: {      // 2. Calculate the percentage of guesture      CGSize screenSize = [UIScreen mainScreen].bounds.size;      CGFloat fraction = translation.x / screenSize.width;      //Limit it between 0 and 1      fraction = fminf(fmaxf(fraction, 0.0), 1.0);      self.shouldComplete = (fraction > 0.4);      [self updateInteractiveTransition:fraction];      break;    }    case UIGestureRecognizerStateEnded:    case UIGestureRecognizerStateCancelled: {      // 3. Gesture over. Check if the transition should happen or not      self.interacting = NO;      if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {        [self cancelInteractiveTransition];      } else {        [self finishInteractiveTransition];      }      break;    }    default:      break;  }}

 以上部分很简单而且并不是我们所关心的部分(虽然名字有点难记忆,但是不难理解)。完成以上代码,就可以实现滑动返回+右进右出的present样式了。

//还有一点需要注意的是,需要禁止全屏手势在导航控制器栈里有多个元素时响应- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{  if ([self.presentingVC isKindOfClass:[BaseNavigationController class]]) {    BaseNavigationController *nav = (BaseNavigationController*)self.presentingVC;    if (nav.viewControllers.count >=2) {      return NO;    }  }  return YES;}

 

四、遇到的问题

  我们(或者说大多数APP)都有这样的需求,当收到推送时需要从底部弹出一个ViewController。 但是因为我们是present 出来了一些页面,当收到推送的时候我们并不能一下子知道从哪个ViewController 上present 出来一个需要用户响应的控制器。比方说  HomeViewController  ---(右进右出present)---->ReportMessageViewController后,从代码的角度我们不能一下子拿到展现在用户面前的控制器(因为你不知道是否已经present了,也不知道谁调用的present)。因此我遇到了第一个问题(很多的项目可以通过tab、nav 来确定,而且present 出控制器的场景比较固定):

  1、到底谁是当前正在响应的控制器?

要想解决这个问题我们首先要知道响应者链条。即当我们点击了屏幕上的一个按钮,事件是怎么传递的。UIView 和 UIViewController 都是继承自UIResponder的,他们都可以成为响应者。因此我们只要遍历屏幕上最上方的那些View(叶子节点),纵向循环找到他们的nextResponder,直至找到为UIViewController类的响应者。

// 获取某个view 的叶子 View(一般为Window)/**/+(NSMutableArray *)getTopSubViewsWithParentView:(UIView *)rootView{  NSMutableArray *stack = [NSMutableArray array];  NSMutableArray *leafNodes = [NSMutableArray array];//存放叶子节点  if (rootView.subviews.count == 0) {    return nil;  }  [stack addObjectsFromArray:rootView.subviews];//把根视图的所有第一层子视图入栈  while (stack.count != 0) {    UIView *subView = [stack lastObject];//取出顶部元素并判断是否为叶子节点    [stack removeLastObject];    if (subView.subviews.count != 0) {//不是叶子节点的话将其子视图继续入栈      [stack addObjectsFromArray:subView.subviews];    }else{      [leafNodes addObject:subView];//如果是叶子节点则将其入栈(叶子节点的栈)    }  }  return leafNodes;}//获取某个视图在哪个控制器上+(UIViewController*)getViewControllerWithView:(UIView*)view{    UIResponder *res = view;  while (res) {    if (res.nextResponder) {      res = res.nextResponder;    }    if ([res isKindOfClass:[UIViewController class]]) {            UIViewController *vc = (UIViewController*)res;      return vc;    }  }  return nil;}//这段代码的意思是,如果我能判断的更精确就精确些。比如某个导航控制器,你说他在响应也行,他的top元素在响应也行,显然我想精确到top元素+(UIViewController*)getCurrentVC{  UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;    NSMutableArray *array = [self getTopSubViewsWithParentView:keyWindow];  UINavigationController *nav = nil;  UITabBarController *tab = nil;  for (UIView *subView in array) {        UIViewController *vc = [self getViewControllerWithView:subView];    if (!([vc isKindOfClass:[UINavigationController class]] || [vc isKindOfClass:[UITabBarController class]])) {      return vc;    }    if ([vc isKindOfClass:[UINavigationController class]]) {      nav = (UINavigationController*)vc;    }    if ([vc isKindOfClass:[UITabBarController class]]) {      tab = (UITabBarController *)vc;    }  }  if (nav) {    return nav;  }  if (tab) {    return tab;  }  return nil;}

 有了这些代码,问题一就解决了。

  2、全屏滑动遇到了可以左右滑动的ScrollView怎么办?

首先说明一下,这个问题我解决的并不完美,因为去除了bounces 效果。因为滑动ScrollView时,ScrollView 的pan 手势会优先响应,并阻止其他手势响应。首先我们先看一下手势代理,看看都有哪些方法:

 1 @protocol UIGestureRecognizerDelegate <NSObject> 2 @optional 3 // called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed 4 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer; 5  6 // called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other 7 // return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously) 8 // 9 // note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES10 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;//手势1、手势2 是否可以共存,即两者都响应,收到事件继续传递下去11 12 // called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies13 // return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer14 //15 // note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES16 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);//在other 响应的情况下,自己是否不响应
17 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);18 19 // called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch20 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;21 22 // called before pressesBegan:withEvent: is called on the gesture recognizer for a new press. return NO to prevent the gesture recognizer from seeing this press23 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

 

通过打印scrollView 的pan手势我们会发现 pan手势的代理是scrollView ,我尝试过改变pan 的delegate  但是发现会崩溃,apple 是不允许改变这个delegate 的。所以我想到了写一个UIScrollView的子类。

//// PanScrollView.m// Property//// Created by 邓竹立 on 16/7/25.// Copyright © 2016年 乐家园. All rights reserved.//#import "PanScrollView.h"@implementation PanScrollView-(instancetype)init{  if (self = [super init]) {    self.bounces = NO;  }  return self;}/* 是否将相应传递给other  当偏移量X值为0的时候全屏手势和pan 手势同时响应。全屏手势向右滑动时 ,由于bounces效果已经被去掉了,所以偏移量不变,ViewController 只有dismiss 效果。当向左滑动时全屏手势没有任何效果,只要scroll效果。 */- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{    if (gestureRecognizer == self.panGestureRecognizer && otherGestureRecognizer == self.otherGes){    if (self.contentOffset.x==0) {      return YES;    }  }  return NO;}@end

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