【转】iOS右滑返回手势全解和最佳实施方案

序言

   在ios7以后,苹果推出了手势滑动返回功能,也就是从屏幕左侧向右滑动可返回上一个界面。大大提高了APP在大屏手机和iPad上的操作体验,场景切换更加流畅。做右滑返回手势配置时,可能会遇到的 问题:
   1. 右滑返回手势为什么失效?
   2. 右滑返回手势如何全局开启及怎么避免页面卡死?
   3. 特定页面停用右滑手势后如何再次开启?
   4. 右滑返回手势与滚动视图手势冲突怎么解决?
   5. 全屏右滑返回怎么设置?

问题分析

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

   右滑返回手势失效主要是因为自定义了页面中navigationItem的leftBarButtonItem或leftBarButtonItems,或是self.navigationItem.hidesBackButton = YES;隐藏了返回按钮,亦或是self.navigationItem.leftItemsSupplementBackButton = NO;,让我们来梳理下。    
    UINavigationItem(Apple文档)是一个常见的类,然而还有不少开发者对该类了解甚少,这里注重说明下 backBarButtonItemleftBarButtonItemrightBarButtonItemleftItemsSupplementBackButton四个属性。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

右滑返回的全屏幕设置

   随着手机屏幕的变大,原来右滑返回略显不够人性化,尤其是对手小的朋友,如何能愉快的单手玩手机呢。对于app要全屏右滑或保持原生边缘触发,各有说辞,这里不讨论其好坏,根据产品需要而定。我们在方案一的基础上,创建一个屏幕手势,添加到原来的self.interactivePopGestureRecognizer.view 右滑返回手势的视图上,即是讲手势添加到VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers数组中, 添加手势必须在设置代理之前完成

- (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;
    }
}
    注意: 系统在self.interactivePopGestureRecognizer.view上已经添加有VC.navigationController.interactivePopGestureRecognizer手势,也可以在VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers数组中取出,此时数组中,有两个响应手势。因此对方案一中的手势控制就要使用数组形式的处理方式。
for (UIGestureRecognizer *popGesture in VC.navigationController.interactivePopGestureRecognizer.view.gestureRecognizers) {
            popGesture.enabled = NO;
        }

总结

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值