前言

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

代码里的设置是:

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

可以看到苹果给navigationController添加了一个手势(具体为UIScreenEdgePanGestureRecognizer(边缘手势,同样是ios7以后才有的)),就是利用这个手势实现的 iOS7的侧滑返回。

但在日常开发中,我们大多会自定义返回按钮,此时系统的右滑返回就会失效。然而支持滑动返回已成为iOS上必须实现的交互,若没有那APP离被卸载就不远了。

设置interactivePopGestureRecognizer

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

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

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

最佳方案

最佳方案参考自sunnyxx的博客,他是怎么做的呢。
通过设置interactivePopGestureRecognizer可以简单的实现,但又会出现很多问题,所以我们可以自己实现一个手势去替换掉系统的,运用

  • runtime+KVC+AOP

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

实现

还是写码最省事,直接动手!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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里定义一个私有类,设置手势的执行条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+(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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (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方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (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/