利用Runtime自定义导航控制器返回手势

自从iOS7之后,系统的导航控制器就具备了边缘滑动返回的功能,这使得用户能够很方便的退出当前页面,大屏的用户不用再费力的去点击导航栏上的返回按钮,很是人性化。但是有些用户觉得这样还是不方便,只能从边缘滑动哪行啊,我要全屏都能滑!于是乎,很多应用,比如QQ、知乎等都实现了这一功能。想要实现这一功能,有好多种方法,而本文介绍的这种方法,是比较好玩的一种方法,因为我们用到了苹果私有的API。虽然违反了苹果的审核政策,但我们自有办法能躲过苹果的检测。下面,就来聊一下实现过程。

首先,我们需要知道系统的侧滑手势是如何实现的。

进入UINavigationController的头文件里,会发现有一个手势interactivePopGestureRecognizer,它是只读的。为了详细的了解这个属性,我们在控制台打印一下它,看看它到底是一个什么手势。

 <
   UIScreenEdgePanGestureRecognizer: 0x7f99d1e10ba0;
   state = Possible;
   delaysTouchesBegan = YES; 
   view = <UILayoutContainerView 0x7f99d1e0b7f0>; 
   target= <(action=handleNavigationTransition:, 
   target=<_UINavigationInteractiveTransition 0x7f99d1e0fc10>)>
   >

可以看到,这个手势属于UIScreenEdgePanGestureRecognizer这个类,它继承自UIPanGestureRecognizer,是专门处理边缘手势的一个类。我们可以通过打印发现它的target:_UINavigationInteractiveTransition(这是一个私有的类,用于处理导航栏动画的),action:handleNavigationTransition:(这个就是系统实现导航栏动画的私有方法)。我们要做的,就是自己新建一个UIPanGestureRecognizer手势,让它的targetaction和系统的相同。

首先,我们需要先获取系统的侧滑手势的target,用常规的手法肯定是获取不到的,因为这是系统私有属性。我们需要用runtime遍历它的成员变量,看一下系统是如何存储这个属性的。

    unsigned int count;
    Ivar *ivar = class_copyIvarList([UIGestureRecognizer class], &count);
    for (int i = 0; i < count; i++) {
        Ivar var = ivar[i];
        NSLog(@"type:===>%s",ivar_getTypeEncoding(var));
        NSLog(@"name:===>%s",ivar_getName(var));
    }

下面是打印的结果,我只取了两条有用的结果:

2015-09-24 15:10:30.879 Nav[1897:149271] type:===>@"NSMutableArray"
2015-09-24 15:10:30.879 Nav[1897:149271] name:===>_targets

我们再来打印一下这个_targets数组,看看里面是什么:

    NSMutableArray *_targets = [systemPopGes valueForKey:@"_targets"];
    NSLog(@"%@",_targets);

打印结果如下:

("(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7fcd0b5195c0>)")

可以看到,可变数组里存储的,就是系统实现导航栏动画的targetaction,获取这个数组的key就是_targets

所以,我们可以通过KVC获取系统存储这个target-action的数组,然后获取系统的target-action,自己创建一个滑动手势,加入到系统实现侧滑手势所在的view中,禁用系统的侧滑手势,我们自定义的手势就可以代替系统的手势,实现滑动了。

下面是实现代码:

#import "JXLNavigationController.h"
#import <objc/runtime.h>

@interface JXLNavigationController ()<UIGestureRecognizerDelegate>

@end

@implementation JXLNavigationController

- (void)viewDidLoad {
    [super viewDidLoad];
    //获取系统侧滑手势
    UIGestureRecognizer *systemPopGes = self.interactivePopGestureRecognizer;
    //禁用系统侧滑
    systemPopGes.enabled = NO;
    //得到系统target-action数组
    NSMutableArray *_targets = [systemPopGes valueForKey:@"_targets"];
    //取出系统实现侧滑的target
    id systemPanTarget = [_targets.firstObject valueForKey:@"target"];
    //获取系统实现侧滑的action
    SEL systemAction = NSSelectorFromString(@"handleNavigationTransition:");
    //自定义滑动手势
    UIPanGestureRecognizer *myPan = [[UIPanGestureRecognizer alloc] initWithTarget:systemPanTarget action:systemAction];
    myPan.delegate = self;
    myPan.maximumNumberOfTouches = 1;
    //向系统实现侧滑的view中加入自定义的滑动手势
    [systemPopGes.view addGestureRecognizer:myPan];
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    //在根视图或者正在滑动时禁用手势
    return self.viewControllers.count != 1 && ![[self valueForKey:@"_interactiveTransition"] boolValue];
}

@end

以上就是简单的实现了一个自定义导航栏滑动手势的UINavigationController,只要继承这个导航控制器,就可以全局实现全屏侧滑手势,当然系统版本一定要在iOS7.0以上才行。

在刚开始的时候我说到这个方法涉及苹果私有API,在发布时可能有被拒的风险,我们可以通过下面的方法简单的避免。

    NSString *selectorStringBegin = @"handleNavigation";
    NSString *selectorStringEnd = @"Transition:";
    NSString *selectorString = [NSString stringWithFormat:@"%@%@",selectorStringBegin,selectorStringEnd];
    SEL systemAction = NSSelectorFromString(selectorString);