滑动返回手势探究
前言
从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