序言
在iOS7之后,苹果推出了手势滑动返回功能,也就是从屏幕左侧向右滑动可返回上一个界面。大大提高了APP在大屏手机和iPad上的操作体验,场景切换更加流畅。做右滑返回手势配置时,可能会遇到的问题:
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,若配置leftBarButtonItem,还需要有返回按钮和右滑手势,需要在leftBarButtonItem或leftBarButtonItems后,把leftItemsSupplementBackButton,设置为YES。
特定页面停用右滑手势?
如左右分页浏览、看视频、看音频、支付等特定页面场景,是“不希望”用户便捷离开的,或有弹窗提示的需求,也有避免用户误操作的考虑。同时,可能存在右滑返回手势冲突,或右滑返回后可能有音频焦点不能及时释放的问题。怎么做呢?我们可以通过代码设置停用右滑返回手势,或改用presentViewController方式加载页面。
恢复右滑手势的解决方案
方案一 手势代理替换
系统自带返回箭头和上级页面title的返回按钮,我们无需设置,系统自动生成,默认tintColor为蓝色。然而,这样的样式并不是我们想要的。我们通常做法是去设置该页面的leftBarButtonItem或leftBarButtonItems,来自定义返回按钮的样式。通过上面的问题分析可知leftBarButtonItem或leftBarButtonItems 会直接覆盖self.navigationController.navigationBar.backItem.backBarButtonItem,造成右滑返回手势响应代理从当前页面被覆盖性移除,造成右滑返回手势失效。我们可以通过在上个页面设置self.navigationItem.backBarButtonItem,并在下个页面设置self.navigationItem.leftItemsSupplementBackButton = YES,来开启右滑返回手势功能。没有做基类管理的项目可能到处都是自定义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;
按照上面的创建思路,已经完成页面自定义返回按钮,并保留了右滑返回手势(注意:导航栏的左侧是不只支持右滑返回手势,这里和方案一有一点区别)。在AViewController push BViewController 或 CViewController 都不需要在再重定义leftBarButtonItem,来实返回按钮了。依次实现各个控制器的backBarButtonItem,即可完成整个APP的右滑返回手势功能,当然以上代码我们可以封装到一个UIViewController基类并在ViewDidLoad方法中来统一设置,或者封装一个工具方法统一调用,当新的页面页面需要不同的返回样式时,在push页面CViewController之前,重新创建backBarButtonItem覆盖即可。
注意:因系统backBarButtonItem中封装的UIButton使用的左图右标题的布局样式和通常的UIButton上图下标题的布局样式有一定的差别,造成即使标题为空,返回按钮的图标的位置依然偏左,我们可以通过UIBarButtonItem的UIBarButtonSystemItemFixedSpace来调图标位置或者设置占位符标题增大手势响应区域。
特定页面停用右滑手势或左侧新添按钮
怎么做呢?自定义leftBarButtonItem或leftBarButtonItems,并设置leftItemsSupplementBackButton = YES。参考代码:
//自定义返回按钮
UIButton *studySearch = [UIButton buttonWithType:UIButtonTypeCustom];
[studySearch setImage:[UIImage imageNamed:@"study_search"] forState:UIControlStateNormal];
[studySearch sizeToFit];
[studySearch addTarget:self action:@selector(studySearchAction) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *studySearchItem = [[UIBarButtonItem alloc] initWithCustomView:studySearch];
self.navigationItem.leftBarButtonItems = @[studySearchItem];
//是否支持显示左滑返回按钮,NO不显示:leftBarButtonItems覆盖backBarButtonItem,
//YES显示:backBarButtonItem 显示在leftBarButtonItems左侧
self.navigationItem.leftItemsSupplementBackButton = YES;
leftItemsSupplementBackButton必须在自定义leftBarButtonItem或leftBarButtonItems后才有效。
方案三 完全自定义导航栏
有些项目中的导航栏或导航控制器是完全自定义的,具体的实现的可以参照方案一实施,这里不再做深入探究。
右滑返回引起手势的冲突
方案二不会存在方案一中的卡死现象。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 = 50;
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
右滑返回的全屏幕设置
随着手机屏幕的变大,原来右滑返回略显不够人性化,尤其是iPhone plus,如何能愉快的单手操作APP,对于APP要实现全屏右滑或保持原生边缘触发,各有说辞,这里不讨论其好坏,根据产品需要而定。我们在方案一的基础上,创建一个屏幕手势,添加到原来的self.interactivePopGestureRecognizer.view 右滑返回手势的视图上,即是将手势添加到VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers数组中,添加手势必须在设置代理之前完成。
- (void)viewDidLoad
{
[super viewDidLoad];
//设全屏启动右滑返回手势,此处可以优化为iPad 上支持全屏
if ((UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)) {
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];
}
//设置右滑返回手势的代理为自身
__weak typeof(self) weakself = self;
if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.interactivePopGestureRecognizer.delegate = (id)weakself;
}
}
注意: 系统在self.interactivePopGestureRecognizer.view上已经添加有VC.navigationController.interactivePopGestureRecognizer手势,也可以在VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers数组中取出,此时数组中,有两个响应手势。因此对方案一中的手势控制就要使用数组形式的处理方式。
for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) {
popGesture.enabled = NO;
}
总结
iOS开发是基于苹果系统的开发,设置系统级全局性的功能时,最好选择系统库或在系统库的基础上自定义,尽量少些自以为是的完全自定义造轮子,少些奇葩设计,好的内容才是一个产品的核心,好的产品体验是用户留存的粘合剂!
最后留一个问题:
场景:从购物车购买商品,下单去订单页面,付款成功后,去了支付成功页面若再返回,则不应该返回订单页面,应该去购物车,甚至回到首页,若用右滑返回手势,怎么监控事件,怎么拦截替换响应事件,或其他途径。
我也在整理这个问题的解决思路,欢迎留言,一起完善!