滑动返回手势探究

作者: 大慈大悲大熊猫 | 来源:发表于2015-10-13 10:57 被阅读2377次

    前言

    从iOS7开始,苹果增加了页面右滑返回的效果,具体的是以UINavigationController为容器的ViewController间右滑切换页面。
    代码里的设置是:

    self.navigationController.interactivePopGestureRecognizer.enabled = YES;(default is YES)
    

    可以看到苹果给navigationController添加了一个手势(具体为UIScreenEdgePanGestureRecognizer(边缘手势,同样是ios7以后才有的)),就是利用这个手势实现的 iOS7的侧滑返回。
    但在日常开发中,我们大多会自定义返回按钮,此时系统的右滑返回就会失效。然而支持滑动返回已成为iOS上必须实现的交互,若没有那APP离被卸载就不远了。

    设置interactivePopGestureRecognizer

    对于这种失效的情况,考虑到interactivePopGestureRecognizer也有delegate属性,替换默认的self.navigationController.interactivePopGestureRecognizer.delegate来配置右滑返回的表现也是可行的。我们可以在主NavigationController中设置一下:

    self.navigationController.interactivePopGestureRecognizer.delegate =(id)self
    

    然而这样又会出现很多问题,比如说在rootViewController的时候这个手势也可以响应,导致整个程序页面不响应;push了多层后,快速的触发两次手势,也会错乱。

    最佳方案

    通过设置interactivePopGestureRecognizer可以简单的实现,但又会出现很多问题,所以我们可以自己实现一个手势去替换掉系统的,运用

    • runtime+KVC+AOP

    的方式,用KVC拿到interactivePopGestureRecognizer的target和action,用runtime动态替换掉,面向切面编程,不用在原工程上增删代码。

    实现

    还是写码最省事,直接动手!
    首先,创建一个UINavigationController的分类,再添加UIViewController的分类,在UINavigationController.h里声明自定义的手势,在UIViewController.h里声明pda_interactivePopDisabled是否显示手势和pda_interactivePopMaxAllowedInitialDistanceToLeftEdge手势滑动距左边最大的距离。

    #import <UIKit/UIKit.h>
    
    @interface UINavigationController (PDAPopGesture)
    
    @property (nonatomic, strong, readonly) UIPanGestureRecognizer *pda_popGestureRecognizer;
    
    @end
    
    @interface UIViewController (PDAPopGesture)
    
    @property (nonatomic, assign) BOOL pda_interactivePopDisabled;
    
    @property (nonatomic, assign) CGFloat pda_interactivePopMaxAllowedInitialDistanceToLeftEdge;
    
    @end
    

    在.m里定义一个私有类,设置手势的执行条件。

    #import "UINavigationController+PDAPopGesture.h"
    #import <objc/runtime.h>
    
    @interface PDAFullscreenPopGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate>
    
    @property (nonatomic, weak) UINavigationController *navigationController;
    
    @end
    
    @implementation PDAFullscreenPopGestureRecognizerDelegate
    
    - (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
    {
        // 当为根控制器时,手势不执行。
        if (self.navigationController.viewControllers.count <= 1) {
            return NO;
        }
        
        // 设置一个页面是否显示此手势,默认为NO 显示。
        UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
        if (topViewController.pda_interactivePopDisabled) {
            return NO;
        }
        
        //  手势滑动距左边框的距离超过maxAllowedInitialDistance 手势不执行。
        CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
        CGFloat maxAllowedInitialDistance = topViewController.pda_interactivePopMaxAllowedInitialDistanceToLeftEdge;
        if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) {
            return NO;
        }
        
        // 当push、pop动画正在执行时,手势不执行。
        if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
            return NO;
        }
        
        //  向左边(反方向)拖动,手势不执行。
        CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
        if (translation.x <= 0) {
            return NO;
        }
        
        return YES;
    }
    
    @end
    

    再在UINavigationController的实现里用Method Swizzling替换pushViewController方法。

    +(void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            
            SEL originalSelector = @selector(pushViewController:animated:);
            SEL swizzledSelector = @selector(pda_pushViewController:animated:);
            
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
            
            BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            if (success) {
                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    

    这里需要注意的是Method Swizzling API 提供的三个方法来动态替换类方法或实例方法。

    • class_replaceMethod 替换类方法的定义
    • method_exchangeImplementations 交换 2 个方法的实现
    • method_setImplementation 设置 1 个方法的实现

    而这三个又有些使用上的区别,class_replaceMethod, 当需要替换的方法可能有不存在的情况时,可以考虑使用该方法。method_exchangeImplementations,当需要交换 2 个方法的实现时使用。method_setImplementation 最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。

    所以这里得先确认添加的方法是否存在,举个具体的例子, 假设要替换掉[NSView description]方法,如果NSView 没有实现-description (可选的) 那你就可会得到NSObject的方法。 如果调用method_exchangeImplementations , 你就会把NSObject 的方法替换成你的代码,这显然不是我们想要的。

    所以在这里定义一个BOOL值来接收class_addMethod的返回值,class_addMethod会动态的给类添加方法,若方法fd_viewWillAppear已存在,class_addMethod会返回失败,此时调用method_exchangeImplementations去替换,若不存在,则用class_replaceMethod替换。

    继续实现pda_pushViewController:animated方法

    - (void)pda_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
        if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.pda_popGestureRecognizer])
        {
            //  添加我们自己的侧滑返回手势
            [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.pda_popGestureRecognizer];
            /*
             新建一个UIPanGestureRecognizer,让它的触发和系统的这个手势相同,
             这就需要利用runtime获取系统手势的target和action。
             */
            //  用KVC取出target和action
            NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
            id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
            SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
            
            //  将自定义的代理(手势执行条件)传给手势的delegate
            self.pda_popGestureRecognizer.delegate = self.pda_popGestureRecognizerDelegate;
            //  将target和action传给手势
            [self.pda_popGestureRecognizer addTarget:internalTarget action:internalAction];
            
            //  设置系统的为NO
            self.interactivePopGestureRecognizer.enabled = NO;
        }
        //  执行原本的方法
        if (![self.viewControllers containsObject:viewController]) {
            [self pda_pushViewController:viewController animated:animated];
        }
    }
    

    其中要注意的是将前面定义的手势触发条件的delegate传给pda_popGestureRecognizer的delegate。
    最后补上pda_popGestureRecognizer的getter和pda_popGestureRecognizerDelegate的setter方法。

    - (PDAFullscreenPopGestureRecognizerDelegate *)pda_popGestureRecognizerDelegate
    {
        PDAFullscreenPopGestureRecognizerDelegate *delegate = objc_getAssociatedObject(self, _cmd);
        if (!delegate) {
            delegate = [[PDAFullscreenPopGestureRecognizerDelegate alloc] init];
            delegate.navigationController = self;
            objc_setAssociatedObject(self, _cmd, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        return delegate;
    }
    
    - (UIPanGestureRecognizer *)pda_fullscreenPopGestureRecognizer
    {
        UIPanGestureRecognizer *panGestureRecognizer = objc_getAssociatedObject(self, _cmd);
        if (!panGestureRecognizer) {
            panGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
            panGestureRecognizer.maximumNumberOfTouches = 1;
            objc_setAssociatedObject(self, _cmd, panGestureRecognizer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        return panGestureRecognizer;
    }
    

    后面UIViewController只需要给出pda_interactivePopMaxAllowedInitialDistanceToLeftEdge和pda_interactivePopDisabled的setter和getter即可。

    后记

    大功告成,直接添加到工程里,不用额外代码即可为你的项目添加滑动返回效果,快去试试吧!

    参考链接

    http://blog.sunnyxx.com/2015/06/07/fullscreen-pop-gesture/http://www.jianshu.com/p/d39f7d22db6c

    相关文章

      网友评论

      • 梨仔_Rosie:这两天认真研读了博主的这篇文章,真心佩服!学习了!!!

        不过博主最后的那段是不是变量名错了,是不是应该改成

        - (UIPanGestureRecognizer *)pda_popGestureRecognizer
        {
        UIPanGestureRecognizer *panGestureRecognizer = objc_getAssociatedObject(self, _cmd);
        if (!panGestureRecognizer) {
        panGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
        panGestureRecognizer.maximumNumberOfTouches = 1;
        objc_setAssociatedObject(self, _cmd, panGestureRecognizer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        return panGestureRecognizer;
        }

        附带着把博主未完成的代码补齐,本人小白一枚,有错误请及时纠正!勿喷啊!!

        @Implementation UIViewController(PDAPopGesture)


        static char kk1;
        - (void)setPda_interactivePopDisabled:(BOOL)pda_interactivePopDisabled{
        NSNumber *number = [NSNumber numberWithBool:pda_interactivePopDisabled];
        objc_setAssociatedObject(self, &kk1, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }

        - (BOOL)pda_interactivePopDisabled{
        NSNumber *number = objc_getAssociatedObject(self, &kk1);
        return [number boolValue];
        }


        static char kk2;
        - (void)setPda_interactivePopMaxAllowedInitialDistanceToLeftEdge:(CGFloat)pda_interactivePopMaxAllowedInitialDistanceToLeftEdge{
        NSNumber *number = [NSNumber numberWithFloat:pda_interactivePopMaxAllowedInitialDistanceToLeftEdge];
        objc_setAssociatedObject(self, &kk2, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }

        - (CGFloat)pda_interactivePopMaxAllowedInitialDistanceToLeftEdge{
        NSNumber *number = objc_getAssociatedObject(self, &kk2);
        return [number floatValue];
        }


        @EnD
        o0下一站生活0o:以前我app,不管向左滑动,还是向右,向上,向下。都会触发返回手势。知道我遇到了这段代码:
        // 向左边(反方向)拖动,手势不执行。
        CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
        if (translation.x <= 0) {
        return NO;
        }
        大慈大悲大熊猫:@妞妞程序媛 确实是写错了,感谢回复!这个当时写的比较粗糙,如果对这方面感兴趣的话,可以看看这个库[KMNavigationBarTransition](https://github.com/MoZhouqi/KMNavigationBarTransition/),比我写的好,欢迎交流。

      本文标题:滑动返回手势探究

      本文链接:https://www.haomeiwen.com/subject/nbgncttx.html