我们知道,导航控制器的边缘返回功能使用的是“UIScreenEdgePanGestureRecognizer”类配合交互式动画进行实现。
想要使用全屏滑动返回,可以通过两种方式进行实现:
- 通过苹果提供的交互式动画方式,提供动画对象和交互对象,使用UIPanGestureRecognizer对象控制交互动画的过程,实现全屏滑动返回。
- 使用自定义的UIPanGestureRecognizer对象通过KVC的方式“替换”掉原来的UIScreenEdgePanGestureRecognizer对象,使我们可以通过拖拽手势触发导航控制器原交互方法,以达到目的。
这里只第二种情况进行学习总结。
注意:本文为轻松学习之二——iOS利用Runtime自定义控制器POP手势动画的读后总结,感叹作者思路之妙。
1. 查看手势响应对象的存储方式
我们知道,手势触发是通过“target-action”方式进行。若想要进行手势替换,就需要了解手势是如何在UIGesture类内进行存储的。故我们先通过runtime的API对其一探究竟:
unsigned int ivarCount = 0;
// 指向Ivar对象数组的头指针
Ivar *ivarList = class_copyIvarList([UIGestureRecognizer class], &ivarCount);
for (int i = 0; i < ivarCount; i++) {
Ivar ivar = ivarList[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSLog(@"name = %s, type = %s", name, type);
// 有效数据为: NSMutableArray *_targets
}
free(ivarList);
通过读取UIGestureRecognizer类的实例对象列表,可以查到,所有的手势响应的对象为类型为NSMutableArray的名为“_targets”的私有变量中。而对于内部真正对象的存储方式,我们需要在UIGestureRecognizer的实例中查看。现在,我们随意在UIView实例上添加一个手势,并通过以下方式查看我们定义的target和action是如何存储的:
(lldb) po [ges valueForKey:@"_targets"]
<__NSArrayM 0x60000045e1b0>(
(action=onGes:, target=<DetailViewController 0x7fd2f3e31fb0>)
)
在debug时,我们可以看到_target数组内部为target和action组成的对象,在触发手势时,运行时系统就动态地向target发送action消息完成手势响应。且由于"_targets"是数组类型,可以在运行期间动态修改内部数据。这也就为我们实现全屏手势提供了可能。
2. 探究导航控制器的返回手势
现在,我们转回到导航控制器中,使用同样方式查看其手势实例:
// 获取导航控制器的交互手势对象
UIGestureRecognizer *originGesture = self.navigationController.interactivePopGestureRecognizer;
// 取出存储的响应信息数组
NSArray *targets = [originGesture valueForKey:@"_targets"];
通过调试,可以看到,内部执行的target和action对象如下所示:
(lldb) po [originGesture valueForKey:@"_targets"]
<__NSArrayM 0x604000450ec0>(
(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7fd2f3c15fe0>)
)
其中,target为私有类_UINavigationInteractiveTransition的实例,action为名为“handleNavigationTransition:”的SEL选择器。
3. 手势的替换
在上面,我们已经得到了边缘拖动时绑定的target和执行的SEL,现在我们只要利用runtime的动态性,将原手势与自定义的UIPanGestureRecognizer对象进行替换,使新手势绑定原target和action,并添加到原控制视图中,最后关闭原手势响应,即可完成神不知鬼不觉的“掉包”。完整代码如下:
UIGestureRecognizer *originGesture = self.navigationController.interactivePopGestureRecognizer;
// 原始手势的绑定视图(并非当前视图)
UIView *originGestureView = originGesture.view;
// 关闭原始手势
originGesture.enabled = NO;
NSArray *targets = [originGesture valueForKey:@"_targets"];
id target = targets[0];
// 真正的触发对象
id realTarget = [target valueForKey:@"_target"];
// 真正的回调为名为“handleNavigationTransition:”的SEL
SEL realAction = NSSelectorFromString(@"handleNavigationTransition:");
// 使用自定义的全屏拖拽手势,使触发target和action均为原navigation的边缘手势,即扩大了手势响应范围。
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:realTarget action:realAction];
[originGestureView addGestureRecognizer:gesture];
网友评论