2016年7月30日
导航控制器的左滑返回
默认状态下,系统提供了导航控制器的左滑返回的功能。系统功能的前提是:
- 导航控制器back按钮是系统自带的---不能自定义leftBarButton
- 系统只提供滑动边缘返回,不提供全屏滑动手势
接下来,我们来阐述如何已经自定义leftBarButton的前提下,实现滑动返回。
下面的方法,其实都是巧妙的借用了系统的返回方法。
1 左滑边缘返回
1.1 思考:
当我们自定义了leftBarButton之后,系统的左边缘滑动手势就不起作用了。我们怎么才能做到,自定义leftBarButton之后,还让系统的左边缘滑动手势起作用?
1.2 分析:
我们在自定义手势的时候,手势一旦添加,当触发手势的时候,就会按照action去执行响应的代码,系统提供的左滑动边缘返回的手势也是一样。对手势的监听(是否执行当前手势、执行的过程等)都是通过代理来完成的,也就是通过代理来控制当前手势触发后,是否执行相应代码。那我们就能得出一个结论:<b style="color:red">自定义leftBarButton后系统返回手势失效,根本原因是系统返回手势的代理在其中起作用,我们要做的是,禁止将系统返回手势的代理清空掉。这样手势就一直存在了。</b>
1.3 问题
滑动返回到上一个控制器,其根本原理是,将当前导航控制器的栈顶控制器出栈,pop掉。那么问题来了,当返回到根控制器后,如果再次尝试返回时,就意味着要讲根控制器pop掉,界面就会卡死。正常情况下,是由系统手势的代理去判断当前栈顶控制器是否为根控制器。所以我们的思路是:<b style="color:red">通过清空系统手势的代理来维持手势一致存在,确保左滑返回可用。且,当当前栈顶控制器为非根控制器的时候才清空系统手势的代理,如果是根控制器,就恢复之前的代理(意味着,清空之前要保存)</b>。
1.4 代码示例
CMNavigationController.m
@interface CMNavigationController () <UINavigationControllerDelegate>
@property(nonatomic,strong) id popGestureDelegate; //用来保存系统手势的代理
@end
@implementation CMNavigationController
- (void)viewDidLoad {
[super viewDidLoad];
#warning 我们不能直接将代理置空,需要根据当前栈顶控制器是否是根控制器进行判断。
// self.interactivePopGestureRecognizer.delegate = nil;
#warning 第一步,先保存当前的代理
self.popGestureDelegate = self.interactivePopGestureRecognizer.delegate;
#warning 第二步,成为自己的代理,去监听pop的过程,pop之前判断是否为根控制器
self.delegate = self;
}
#warning 第三步,监听pop的方法,判断当前的栈顶控制器是否为根控制器
-(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
//self.viewControllers[0]表示根控制器
if([self.topViewController isEqual:self.viewControllers[0]])
{
//如果是根控制器,恢复系统手势默认的代理
self.interactivePopGestureRecognizer.delegate = self.popGestureDelegate;
}else
{
//如果是非根控制器,将系统手势的默认代理置空
self.interactivePopGestureRecognizer.delegate = nil;
}
}
2 左滑全屏(任意位置)返回
2.1 思考
就算我们不自定义leftBarButton,系统的手势也没办法帮我们实现全屏返回。<b style="color:red">也就是说,手势必须由我们自己去定义。</b>
2.2 分析
自定义手势的时候,我们只需要定义手势的执行方法action和方法的执行对象target即可。由于全屏返回这个手势功能比较复杂,<b style="color:red">所以我们目前的首要任务不是去自己实现action,而是去借用系统的target对象的action方法。</b>因为,不管是系统的左滑边缘返回手势,还是自定义全屏左滑返回手势,返回这个功能都是一样的。
2.3 问题
目前,我们的任务有两个:①找到系统的action方法名称;②找到系统的target对象(因为action方法是target对象的)。第一个任务很简单,通过打印系统的手势就可以知道方法名称。第二个任务就困难了。有两种方式:①runtime去分析当前系统的属性 ②通过自定义手势的规律去猜测。
2.4 找到action方法名称
第一步,打印系统手势
//1 打印系统手势
/**
* 打印结果
<UIScreenEdgePanGestureRecognizer: 0x7ffaea42b2f0; state = Possible; delaysTouchesBegan = YES; view = <UILayoutContainerView 0x7ffaea787b50>; target= <(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7ffaea42ae20>)>>
*/
NSLog(@"\n%@ \n",self.interactivePopGestureRecognizer);
<b style="color:red">第二步,通过打印结果看,执行action方法是handleNavigationTransition </b>
2.5 找action方法的对象target
2.5.1 运行时runtime分析
第一步,进入系统手势UIScreenEdgePanGestureRecognizer的头文件,以及其父类,父类的父类的头文件。
/**
* 进入头文件的目的是,确认头文件中关于target的说明,最后在UIGestureRecognizer这个超类中找到了关于initWithTarget的字眼,说明target很可能是这个类的私有属性。所以,我们可以通过KVC来获取其私有属性的值,这样就完成了找target的任务。问题来了,通过KVC来获取值,首先得知道属性的名称呀。所以,接下来我们要继续找到这个属性的名称
*/
第二步,找到target的属性
/**
* 1 通过运行时函数,获取类的属性列表
参数:__unsafe_unretained Class cls 表示要获取哪个类的属性列表
unsigned int *outCount 表示属性列表数组的个数,传入的是指针,函数进行修改,我们就可以通过参数获得属性个数
返回值:返回的是属性列表数组
*/
unsigned int outCount;
Ivar *ivars = class_copyIvarList([UIGestureRecognizer class], &outCount);
/**
* 2 遍历,属性列表数组,并打印,观察其中的哪个属性与target有关
结论:打印结果第一条,与其有关。 _targets
*/
for(int i=0;i<outCount;i++)
{
//2.1 获取元素(元素就是该类的属性,是C语言的结构体)
Ivar ivar = ivars[i];
//2.2 获取元素的名称,通过C语言函数获取
const char *ivar_name = ivar_getName(ivar);
//2.3 将C语言字符串包装成OC字符串,打印
NSLog(@"%@",@(ivar_name));
}
/**
* 3 根据第二步的打印结果,通过KVC将_targets的值取出来。并打印。
*
* 结论,打印结果是一个数组,且只有一个元素(该元素是一个字典),将其取出来
(
"(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7f8e7b54c540>)"
)
*/
id _targets = [self.interactivePopGestureRecognizer valueForKeyPath:@"_targets"];
NSLog(@"%@",_targets);
/**
* 4 将上一步的数组第一个元素取出来,获取内部字典的target的属性值--这个属性值就是我们要找的target对象
*/
NSDictionary *dict = _targets[0];
id temp = [dict valueForKeyPath:@"target"];
NSLog(@"%@",temp);
第三步,自定义手势
到这里,我们已经找到了action=handleNavigationTransition: target=temp
//1 自定义手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:temp action:@selector(handleNavigationTransition:)];
//2 添加手势
[self.view addGestureRecognizer:pan];
2.5.2 根据自定义手势的规律猜测
我们在自定义手势的时候,一般是这样的
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGetsture:)];
也就是说,执行手势的对象一般是self(而对于手势的监控室代理,所以是当前手势的代理)。我们可以推测,系统手势也是这样的,通过系统手势的代理来执行相对应的方法。<b style="color:red">所以tartget对象为:self.interactivePopGestureRecognizer.delegate</b>
2.6 问题
因为,目前手势有我们自己的自定义的,我们并没有判断当前是否是根控制器,也就是说当前手势在根控制器也会生效。当我们在根控制器滑动返回的时候,系统仍然会将当前控制器pop掉,但事实上,根控制器是不能被Pop掉的,不然会出现卡死的现象。所以,我们需要监控手势的状态,在即将执行手势之前,我们判断是否为根控制器,如果是,不执行手势。所以需要设置自定义手势的代理为自己。
pan.delegate = self;
//遵守协议UIGetstureRecognizerDelegate,实现代理方法
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
//在每次手势开始之前,调用这个方法
if([self.topViewController isEqual:self.viewControllers[0]])
{
//如果当前栈顶控制器是根控制器,则返回NO,不执行手势
return NO ;
}
else{
//如果是非根控制器,执行首饰
return YES;
}
}