序言



问题:



   1. 右滑返回手势为什么失效?



   2. 右滑返回手势如何全局开启及怎么避免页面卡死?



   3. 特定页面停用右滑手势后如何再次开启?



   4. 右滑返回手势与滚动视图手势冲突怎么解决?



   5. 全屏右滑返回怎么设置?



问题分析

右滑返回手势为什么失效?



   右滑返回手势失效主要是因为自定义了页面中navigationItem的leftBarButtonItem或leftBarButtonItems,或是self.navigationItem.hidesBackButton = YES;隐藏了返回按钮,亦或是self.navigationItem.leftItemsSupplementBackButton = NO;,让我们来梳理下。    


    UINavigationItem(Apple文档)是一个常见的类,然而还有不少开发者对该类了解甚少,这里注重说明下 backBarButtonItem、 leftBarButtonItem、 rightBarButtonItem和 leftItemsSupplementBackButton四个属性。leftBarButtonItem、rightBarButtonItem是在当前页面设置,并展示在 当前页面的navigationItem上。backBarButtonItem若是在当前页面设置,却展示在 次级页面navigationItem上。


   比如在AViewController push BViewController时,在A设置了self.navigationItem.backBarButtonItem的title和image,经过试验发现,这个backBarButtonItem为BViewController的self.navigationController.navigationBar.backItem.backBarButtonItem。虽然self.navigationController.navigationBar.backItem.backBarButtonItem 是读写属性,但是self.navigationController、self.navigationController.navigationBar、 self.navigationController.navigationBar.backItem,都是readonly属性,因此backBarButtonItem,只能在AViewController中定义并在Push:BViewController之前进行设置。leftBarButtonItem、rightBarButtonItem可以在BViewController的ViewDidLoad后设置。



注意: backBarButtonItem只能自定义image和title,不能重写target 或 action,系统会忽略其他的相关设置项。如果硬是需要重写action做一些其他的工作,则需要自定义一个leftBarButtonItem。    系统默认情况下leftBarButtonItem的优先级是要高于backBarButtonItem的,当存在leftBarButtonItem时,自动忽略backBarButtonItem,达到重写backBarButtonItem的目的,但会造成右滑返回手势的响应代理从当前页面被覆盖性移除。同时,系统也提供了leftItemsSupplementBackButton属性来控制backBarButtonItem 是否和 leftBarButtonItem 并列显示,默认值是NO. 若设置为YES, 在设置leftBarButtonItem后, 将会保留backBarButtonItem以及右滑手势.



特定页面停用右滑手势?



   如左右分页浏览、看视频、看音频、支付等特定页面场景,是“不希望”用户便捷离开的,或有弹窗提示的需求,也有避免用户误操作的考虑。同时,可能存在右滑返回手势冲突,或右滑返回后可能有音频焦点不能及时释放的问题。怎么做呢?我们可以通过代码设置停用右滑返回手势,或改用presentViewController方式加载页面。



自定义leftBarButtonItem之后, 恢复右滑手势的解决方案

方案一 手势代理替换



   系统的自带的有返回箭头和上级页面title的返回按钮,我们无需设置,系统自动生成,默认tintColor为蓝色。然而,这样的样式并不是我们想要的。我们通常做法是去,设置该页面的leftBarButtonItem或leftBarButtonItems,来自定义返回按钮的样式。通过上面的问题分析,我们可以知道,leftBarButtonItem或leftBarButtonItems 直接覆盖了self.navigationController.navigationBar.backItem.backBarButtonItem,造成右滑返回手势的响应代理从当前页面被覆盖性移除,造成右滑返回手势失效。没有做基类管理的项目可能到处都是自定义leftBarButtonItem或leftBarButtonItem,工作量较大。快上车,让老司机带你一程!



保留系统的右滑返回手势



   既然设置backBarButtonItem较为繁杂,我们可以换个思路,手势已被覆盖性移除,我们需要给页面添加上右滑返回手势。若项目有全局的UINavigationController基类,实现下列参考代码:


@implementation YGNavigationController 
 
 
 
 
  
 
 

   - (void)viewDidLoad 
 
 
 

   { 
 
 
 

       [super viewDidLoad]; 
 
 
 

       //设置右滑返回手势的代理为自身 
 
 
 

       __weak typeof(self) weakself = self; 
 
 
 

       if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) { 
 
 
 

           self.interactivePopGestureRecognizer.delegate = (id)weakself; 
 
 
 

       } 
 
 
 

   } 
 
 
 
 
  
 
 

   #pragma mark - UIGestureRecognizerDelegate 
 
 
 

   //这个方法是在手势将要激活前调用:返回YES允许右滑手势的激活,返回NO不允许右滑手势的激活 
 
 
 

   - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 
 
 
 

   { 
 
 
 

       if (gestureRecognizer == self.interactivePopGestureRecognizer) { 
 
 
 

           //屏蔽调用rootViewController的滑动返回手势,避免右滑返回手势引起死机问题 
 
 
 

           if (self.viewControllers.count < 2 || 
 
 
 

   self.visibleViewController == [self.viewControllers objectAtIndex:0]) { 
 
 
 

               return NO; 
 
 
 

           } 
 
 
 

       } 
 
 
 

       //这里就是非右滑手势调用的方法啦,统一允许激活 
 
 
 

       return YES; 
 
 
 

   }

   将项目中的使用UINavigationController 替换为UINavigationController基类,自定义返回按钮设置不变,恢复了右滑返回手势。注意:导航栏的左侧也是支持右滑返回手势,若有UIViewController基类也可以参照上面设置代码调整设置,来消除导航栏的左侧小区域的右滑返回。



一定要实现UIGestureRecognizerDelegate 并做rootViewController 判断,否则,在rootViewController页面会存在右滑返回死机的问题。



特定页面停用右滑手势



   我们查看UINavigationController 文档,可以找到


@property(nullable, nonatomic, readonly) UIGestureRecognizer *interactivePopGestureRecognizer NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;


   可以通过设置页面的VC.navigationController.interactivePopGestureRecognizer.enabled 来控制当前页面的右滑返回手势是否可用。我们可以创建一个UIViewController 的分类创建两个类方法。



+ (void)popGestureClose:(UIViewController *)VC 
 
 
 

   { 
 
 
 

       // 禁用侧滑返回手势 
 
 
 

       if ([VC.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) { 
 
 
 

           //这里对添加到右滑视图上的所有手势禁用 
 
 
 

           for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) { 
 
 
 

               popGesture.enabled = NO; 
 
 
 

           } 
 
 
 

           //若开启全屏右滑,不能再使用下面方法,请对数组进行处理 
 
 
 

           //VC.navigationController.interactivePopGestureRecognizer.enabled = NO; 
 
 
 

       } 
 
 
 

   } 
 
 
 
 
  
 
 

   + (void)popGestureOpen:(UIViewController *)VC 
 
 
 

   { 
 
 
 

       // 启用侧滑返回手势 
 
 
 

       if ([VC.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) { 
 
 
 

       //这里对添加到右滑视图上的所有手势启用 
 
 
 

           for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) { 
 
 
 

               popGesture.enabled = YES; 
 
 
 

           } 
 
 
 

           //若开启全屏右滑,不能再使用下面方法,请对数组进行处理 
 
 
 

           //VC.navigationController.interactivePopGestureRecognizer.enabled = YES; 
 
 
 

       } 
 
 
 

   } 
 
 
 

      具体怎么使用呢?我们需要在停用右滑返回手势的页面实现以下两个方法,经过多次调试验证,必须是以下两个方法。停用当前页面后,不影响上级页面和下级页面的右滑返回。 
 
 
 

   - (void)viewDidAppear:(BOOL)animated 
 
 
 

   { 
 
 
 

       [super viewDidAppear:animated]; 
 
 
 

       [UIViewController popGestureClose:self]; 
 
 
 

   } 
 
 
 
 
  
 
 

   - (void)viewWillDisappear:(BOOL)animated 
 
 
 

   { 
 
 
 

       [super viewWillDisappear:animated]; 
 
 
 

       [UIViewController popGestureOpen:self]; 
 
 
 

   }





方案二 原生态:自定义backBarButtonItem(图片)



   网上的思路大多是基于方案一,这是我在研究方案一中回溯思路得出的一个方案,直接利用系统的backBarButtonItem和右滑返回手势特性,相对更稳定,更高效,我想iOS系统APP的右滑返回设计应是这个“官方思路”。



保留系统的右滑返回手势



   这里需要对每个页面设置自己的backBarButtonItem,就像设置每个页面的leftBarButtonItem的思路一样。但是backBarButtonItem是一个特殊的按钮,可以说只响应页面的返回和销毁,表现为只能自定义image和title,不能重写target 或 action。来让我们自定义以下backBarButtonItem。参照问题分析的思路,须在AViewController中实现下列参考代码:



 

UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; 
 
 
 

       //自定义返回按钮的视图,如细化返回图标。 
 
 
 

        [self.navigationController.navigationBar setBackIndicatorImage:[UIImage imageNamed:@"navi_back_icon"]]; 
 
 
 

        [self.navigationController.navigationBar setBackIndicatorTransitionMaskImage:[UIImage imageNamed:@"navi_back_icon"]]; 
 
 
 

        //设置tintColor 改变自定图片颜色 
 
 
 

        self.navigationController.navigationBar.tintColor = [UIColor whiteColor]; 
 
 
 

        //设置自定义的返回按钮 
 
 
 

        self.navigationItem.backBarButtonItem = backItem;



   按照上面的创建思路,已经完成页面自定义返回按钮,并保留了右滑返回手势(注意:导航栏的左侧是不支持右滑返回手势的,这里和方案一有一点区别)。在A push B 或 C 都不需要在再重定义leftBarButtonItem,来实返回按钮了。依次实现各个控制器的backBarButtonItem,即可完成整个APP的右滑返回手势功能,当然以上代码我们可以封装到一个UIViewController基类并在ViewDidLoad方法中来统一设置,或者封装一个工具方法统一调用,当新的页面页面需要不同的返回样式时,在push页面C之前,重新创建backBarButtonItem覆盖即可。



方案三 完全自定义导航栏



   有些项目中的导航栏或导航控制器是完全自定义的,具体的实现的可以参照方案一实施,这里不再做深入探究。



右滑返回引起手势的冲突



   方案二不会存在方案一中的卡死现象。iOS系统中,滑动返回手势其实是一个UIPanGestureRecognizer,UIScrollView的滑动手势也是UIPanGestureRecognizer,UIPanGestureRecognizer接收顺序和UIView的层次结构是一致的。



UINavigationController.view —>  UIViewController.view —>  UIScrollView —>  Screen and User's finger



原理:UIScrollView(包括子类UITextView、UITableView、UICollectionView)的panGestureRecognizer先接收到手势事件,直接处理后不在往下传递。实际上这就是两个panGestureRecognizer共存的问题。scrollView的pan手势会让系统的pan手势失效,当UIScrollView(UICollectionView)有多页的时候也会出现滑动返回失效的情况,我们需要在scrollView的位置在初始位置的时候,让两个手势同时启用。 可以创建UIScrollView的类别category,然后在此类别中实现以下方法即可:


#import "UIScrollView+PopGesture.h" 
 
 
 
 
  
 
 

   @implementation UIScrollView (PopGesture) 
 
 
 
 
  
 
 

   //此方法返回YES时,手势事件会一直往下传递,不论当前层次是否对该事件进行响应。 
 
 
 

   - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
 
 
 

   shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 
 
 
 

   { 
 
 
 

       if ([self panBack:gestureRecognizer]) { 
 
 
 

           return YES; 
 
 
 

       } 
 
 
 

       return NO; 
 
 
 

   } 
 
 
 
 
  
 
 

   //location_X可自己定义,其代表的是滑动返回距左边的有效长度 
 
 
 

   - (BOOL)panBack:(UIGestureRecognizer *)gestureRecognizer 
 
 
 

   { 
 
 
 

       //是滑动返回距左边的有效长度 
 
 
 

       int location_X = 40; 
 
 
 

       if (gestureRecognizer == self.panGestureRecognizer) { 
 
 
 

           UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer; 
 
 
 

           CGPoint point = [pan translationInView:self];  //拖动的距离 
 
 
 

           UIGestureRecognizerState state = gestureRecognizer.state; 
 
 
 

           if (UIGestureRecognizerStateBegan == state || UIGestureRecognizerStatePossible == state) { 
 
 
 

               CGPoint location = [gestureRecognizer locationInView:self]; //手势所在的 
 
 
 

               //下面的是只允许在第一张时滑动返回生效 
 
 
 

               if (point.x > 0 && location.x < location_X && self.contentOffset.x <= 0) { 
 
 
 

                   return YES; 
 
 
 

               } 
 
 
 

            //   这是允许每张图片都可实现滑动返回 
 
 
 

            //   int temp1 = location.x; 
 
 
 

            //   int temp2 = SCREEN_WIDTH; 
 
 
 

            //   NSInteger XX = temp1 % temp2; 
 
 
 

            //   if (point.x > 0 && XX < location_X) { 
 
 
 

            //      return YES; 
 
 
 

            //   } 
 
 
 

           } 
 
 
 

       } 
 
 
 

       return NO; 
 
 
 

   } 
 
 
 
 
  
 
 

   - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 
 
 
 

   { 
 
 
 

       if ([self panBack:gestureRecognizer]) { 
 
 
 

           return NO; 
 
 
 

       } 
 
 
 

       return YES; 
 
 
 

   }




@end



右滑返回的全屏幕设置



添加手势必须在设置代理之前完成。



- (void)viewDidLoad 
 
 
 

   { 
 
 
 

       [super viewDidLoad]; 
 
 
 

       //设全屏启动右滑返回手势,此处可以优化为iPad 上支持全屏 
 
 
 

       
 
 
 

           id target = self.interactivePopGestureRecognizer.delegate; 
 
 
 

           SEL handler = NSSelectorFromString(@"handleNavigationTransition:"); 
 
 
 

           // 获取添加系统边缘触发手势的View 
 
 
 

           UIView *targetView = self.interactivePopGestureRecognizer.view; 
 
 
 

           // 创建pan手势 作用范围是全屏 
 
 
 

           UIPanGestureRecognizer *fullScreenGes = [[UIPanGestureRecognizer alloc]initWithTarget:target action:handler]; 
 
 
 

           fullScreenGes.delegate = self; 
 
 
 

           [targetView addGestureRecognizer:fullScreenGes]; 
 
 
 

           // 关闭边缘触发手势 防止和原有边缘手势冲突(也可不用关闭) 
 
 
 

           [self.interactivePopGestureRecognizer setEnabled:NO]; 
 
 
 

           SEL handler = NSSelectorFromString(@"handleNavigationTransition:"); 
 
 
 

           // 获取添加系统边缘触发手势的View 
 
 
 

           UIView *targetView = self.interactivePopGestureRecognizer.view; 
 
 
 

           // 创建pan手势 作用范围是全屏 
 
 
 

           UIPanGestureRecognizer *fullScreenGes = [[UIPanGestureRecognizer alloc]initWithTarget:target action:handler]; 
 
 
 

           fullScreenGes.delegate = self; 
 
 
 

           [targetView addGestureRecognizer:fullScreenGes]; 
 
 
 

           // 关闭边缘触发手势 防止和原有边缘手势冲突(也可不用关闭) 
 
 
 

           [self.interactivePopGestureRecognizer setEnabled:NO]; 
 
 
  
 

       //设置右滑返回手势的代理为自身 
 
 
 

       __weak typeof(self) weakself = self; 
 
 
 

       if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) { 
 
 
 

           self.interactivePopGestureRecognizer.delegate = (id)weakself; 
 
 
 

       } 
 
 
 

   }

注意:


for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) { 
 
 
 

               popGesture.enabled = NO; 
 
 
 

           }

总结



   iOS开发都是基于苹果系统的开发,设置系统级全局性的功能时,最好选择系统或在系统的基础上自定义,尽量少些自以为是的完全自定义,少些奇葩设计,好的内容才是一个产品的核心,好的产品体验也是用户留存的粘合剂!