前言
从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可以简单的实现,但又会出现很多问题,所以我们可以自己实现一个手势去替换掉系统的,运用
的方式,用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; } UIViewController *topViewController = self.navigationController.viewControllers.lastObject; if (topViewController.pda_interactivePopDisabled) { return NO; } CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view]; CGFloat maxAllowedInitialDistance = topViewController.pda_interactivePopMaxAllowedInitialDistanceToLeftEdge; if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) { return NO; } 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。 */ NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"]; id internalTarget = [internalTargets.firstObject valueForKey:@"target"]; SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:"); self.pda_popGestureRecognizer.delegate = self.pda_popGestureRecognizerDelegate; [self.pda_popGestureRecognizer addTarget:internalTarget action:internalAction]; 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/