内容均为原创, 如有任何疑问或者错误,请在文章下留言或者直接与我联系,一定及时回复: )
· 前言
· 框架背景
· 框架知识准备
· 源码解析
· 框架工作流程图
· 前言
由于6Plus的出现,iphone的默认导航栏又是在屏幕顶部,对于app的返回操作大屏手机对于小手的用户来说操作显得不那么友好。iOS7为了提升app的返回体验,增加了边缘侧滑返回手势,但是对于小手用户来说,返回体验没有彻底得到改善。于是开发者们开始绞尽脑汁地想各种办法,其中一种办法,也就是今天要讲的---将返回手势变为全屏的框架。
github千星框架 FDFullscreenPopGesture
这个框架能get到4K+的星星,肯定是有过人之处的。抱着学习的态度,去看了下源码,对于runtime以及封装代码的思路,都有很大的帮助。前言就到这里,下面我会尽量清晰的表达我看源码的思路,尽量把一些知识点说的通俗易懂。
· 框架背景
这个扩展来自 @J_雨 同学的这个很天才的思路,他的文章地址:戳这里
如果不太愿意戳进去看,我这里来总结下这位同学的思路:
1.如果自定义手势的话,还要考虑控制器切换的动画,成本太高太麻烦。
2.iOS7.0之后苹果提供了边缘滑动手势,获取到这个边缘滑动手势,然后把它的触发范围从边缘改成全屏不就好了?然后经过一番折腾,发现没有办法修改,这个方法是不可行的。
3.那么既然不能将它的手势触发的方法找到,我们自己去创建一个全屏手势,去调用那个方法,不就好了?bingo,可行。
4.有人对这个思路进行了封装于是有了FDFullscreenPopGesture
· 框架知识准备
· 为分类添加属性
objc_setAssociatedObject
objc_getAssociatedObject
传送门:Runtime奇技淫巧之objc_setAssociatedObject,objc_getAssociatedObject
· 在 main 函数调用之前被 ObjC 运行时调用的方法,框架只需要放入项目文件夹即可实现框架功能,都是这个钩子方法的功劳。
+(void)load
传送门:Objective-C Method Swizzling 的最佳实践
· NavigationController可以通过调用setViewController方法将画面的跳转历史路径(堆栈)完全替换
传送门:页面的跳转技巧--setViewControllers
· 框架源码解析
打开项目我们能看到,该框架只有一个.h 和.m文件。
.h中只暴露了UINavigationController 和 UIViewController的两个分类属性。
.m中包括四部分
@implementation _FDFullscreenPopGestureRecognizerDelegate : NSObject (私有)
@implementation UIViewController (FDFullscreenPopGesturePrivate)(私有)
@implementation UINavigationController (FDFullscreenPopGesture)
@implementation UIViewController (FDFullscreenPopGesture)
· 下面为大家一一讲解下这四个implementation都干了一些什么事
1.FDFullscreenPopGestureRecognizerDelegate:定义了一个类遵循了手势代理协议,并且有一点navigationController的属性。自定义的手势是否被触发由这个类来控制。
@interface _FDFullscreenPopGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate>
@property (nonatomic, weak) UINavigationController *navigationController;
@end
// 这个类实现了自定义手势的代理方法
@implementation _FDFullscreenPopGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
// 当没有控制器入栈的时候,不触发手势
if (self.navigationController.viewControllers.count <= 1) {
return NO;
}
// 如果控制器的fd_interactivePopDisabled属性为NO不触发手势
//(fd_interactivePopDisabled是作者对UIViewController添加的一个属性,下面会讲)
UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
if (topViewController.fd_interactivePopDisabled) {
return NO;
}
//当手势开始的位置 超出了fd_interactivePopMaxAllowedInitialDistanceToLeftEdge所设定的值,那么就不触发手势
CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
CGFloat maxAllowedInitialDistance = topViewController.fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) {
return NO;
}
// 如果导航控制器正在执行转场动画,则不触发手势
if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
return NO;
}
// 1.这个比较神奇,当app语言设置为阿拉伯语等阅读顺序从右到左的语言,且app的布局适配了这个语种,
// 2.那么导航控制器的入栈动画会由从右到左,调整为从左到右,从作者的代码上来看手势好像是不支持从左到右的app布局的。
// 3.也就是说,当app语言设置为阿拉伯等语言并且app适配了这种布局,不触发手势
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
BOOL isLeftToRight = [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionLeftToRight;
CGFloat multiplier = isLeftToRight ? 1 : - 1;
if ((translation.x * multiplier) <= 0) {
return NO;
}
return YES;
}
@end
- @interface UIViewController (FDFullscreenPopGesturePrivate) 定义了一个block,在ViewControllerWillAppear的时候会被注入
@implementation UIViewController (FDFullscreenPopGesturePrivate)在main函数之前会将系统的viewWillAppear方法和viewWillDisappear方法替换成分类中的方法。
typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);
@interface UIViewController (FDFullscreenPopGesturePrivate)
@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;
@end
const NSString *block = @"block";
@implementation UIViewController (FDFullscreenPopGesturePrivate)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 获取系统的viewWillAppear:方法
Method viewWillAppear_originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
// 获取自定义的fd_viewWillAppear:方法
Method viewWillAppear_swizzledMethod = class_getInstanceMethod(self, @selector(fd_viewWillAppear:));
// 两者交换
method_exchangeImplementations(viewWillAppear_originalMethod, viewWillAppear_swizzledMethod);
// 获取系统的viewWillDisappear:方法
Method viewWillDisappear_originalMethod = class_getInstanceMethod(self, @selector(viewWillDisappear:));
// 获取自定义的viewWillDisappear:方法
Method viewWillDisappear_swizzledMethod = class_getInstanceMethod(self, @selector(fd_viewWillDisappear:));
// 两者交换method_exchangeImplementations(viewWillDisappear_originalMethod, viewWillDisappear_swizzledMethod);
});
}
- (void)fd_viewWillAppear:(BOOL)animated
{
// 1.为了不破坏原本的业务逻辑,先执行原来的viewWillAppear方法
// 2.为什么在fd_viewWillAppear:方法中调用fd_viewWillAppear:方法不会引起死循环?
// 3.因为fd_viewWillAppear:这个方法已经和viewWillAppear:方法做了交换
// 4.所以调用 fd_viewWillAppear:方法相当于调用了viewWillAppear:方法,
// 5.调用viewWillAppear:方法相当于调用了fd_viewWillAppear:方法
[self fd_viewWillAppear:animated];
// 执行注入的block 这个block到底干了什么事情,会在后面讲到
if (self.fd_willAppearInjectBlock) {
self.fd_willAppearInjectBlock(self, animated);
}
}
- (void)fd_viewWillDisappear:(BOOL)animated
{
// 同理,这里不再赘述。
[self fd_viewWillDisappear:animated];
// 1.当用户有pop或者push操作的时候,
// 2.根据导航栏栈顶控制的fd_prefersNavigationBarHidden这个分类属性,
// 3.控制导航栏是否需要隐藏
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIViewController *viewController = self.navigationController.viewControllers.lastObject;
if (viewController && !viewController.fd_prefersNavigationBarHidden) {
[self.navigationController setNavigationBarHidden:NO animated:NO];
}
});
}
- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
// 1. 调用fd_willAppearInjectBlock属性的get方法的时候
// 2. 会在本类中以该get方法的名称为key,找到对应的value,也就是该block的值
// 3. 更多关于runtime为分类添加属性的知识,请看框架知识储备
return objc_getAssociatedObject(self, _cmd);
}
- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
{
// 1. 当调用了fd_willAppearInjectBlock这个分类属性的set方法时候,
// 2. 会以block为value 以该属性的get方法为key将block存储起来
// 3. 以后就可以通过调用fd_willAppearInjectBlock属性的get方法,获取block
objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
3.全屏手势的核心返回功能在此实现,交换push方法后,将系统返回手势替换为自定义手势,设置代理,如果允许用户根据控制器的分类属性控制导航栏显示或者隐藏,则给入栈的控制器的block赋值。
@implementation UINavigationController (FDFullscreenPopGesture)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 同上,为了注入代码,替换导航控制器的push方法。
SEL originalSelector = @selector(pushViewController:animated:);
SEL swizzledSelector = @selector(fd_pushViewController:animated:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 1.这里要注意,class_addMethod是为了检查本类是否实现了这个方法
// 2.如果方法添加成功,代表本类没有实现该方法(该方法在父类中实现,却没有在子类中实现)
// 3.打个比方,如果我创建了一个testNavgationController继承自UINavigationController
// 4.但是,我自定义的testNavgationController只进行了修改navBar背景色的方法
// 5.自定义的testNavgationController并没有重写 pushViewController:animated:
// 6.这个时候,如果我直接调用方法交换的话,会和父类中的pushViewController:animated:交换
// 7.显然,这不是我们想要的结果,我们只希望和testNavigation的pushViewController:animated:方法交换
// 8.所以先调用class_addMethod方法,检查本类是否实现了pushViewController:animated:
// 9.如果实现了,那很好,直接交换
// 10.如果没实现,那么class_addMethod已经把push方法 (对应的实现是fd_push)添加到了本类
// 11.我们只需要再调用class_replaceMethod方法添加fd_push(对应的实现是push) 添加到本类
// 12.这样,就达到了方法交换的目的
// 13.pushViewController:animated: 的内部实现为fd_pushViewController:animated:
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
// 1. class_replaceMethod方法会检查是否存在swizzledSelector这个方法名
// 2.如果存在,那么将原来实现替换为 originalMethod
// 3.如果不存在 则会先 添加这个方法名swizzledSelector,
// 4.然后再添加这个方法的实现originalMethod
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
NSLog(@"%s",method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
//打印self.interactivePopGestureRecognizer.view我们会发现它的类型是UILayoutContainerView
// (UILayoutContainerView就是window 上的第一个 subview)
//判断自定义手势是否已经加在了UILayoutContainerView上
[self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];
// 使用自定义手势替换系统边缘返回的手势,
// 原理已经在本文的“框架背景”章节阐述了,这里就不多啰嗦了
NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
[self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];
// 关闭导航控制器自带的边缘返回手势(因为它已经被自定义手势取而代之了)
self.interactivePopGestureRecognizer.enabled = NO;
}
// 这个方法控制了导航控制器中的子控制器是否有独立控制导航栏显示或者隐藏的权利(下面会讲)
// fd_viewControllerBasedNavigationBarAppearanceEnabled属性默认为YES
// 也就是说,默认会根据控制的分类属性fd_prefersNavigationBarHidden来控制栏的隐藏或者显示
// 如果fd_viewControllerBasedNavigationBarAppearanceEnabled为NO
// 那么导航控制器的导航栏的显示与否,控制器无权决定
[self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
if (![self.viewControllers containsObject:viewController]) {
// 调用push方法,将控制器入栈
[self fd_pushViewController:viewController animated:animated];
}
}
- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
{
// 上面已经说了,fd_viewControllerBasedNavigationBarAppearanceEnabled为NO,则直接return
if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
return;
}
// 前面在2.中我们提到过
// UIViewController (FDFullscreenPopGesturePrivate) 定义了一个block
// 从这里我们可以看到,只有在 fd_viewControllerBasedNavigationBarAppearanceEnabled == YES的时候
// 才会给block赋值,才会执行block,
// block中会根据fd_prefersNavigationBarHidden 判断是否要显示或者隐藏导航栏
__weak typeof(self) weakSelf = self;
_FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated];
}
};
// 1.对即将入栈的控制器的fd_willAppearInjectBlock属性进行赋值
// 2.在push前,也对栈顶的控制器fd_willAppearInjectBlock赋值
// 3.请注意,这个时候栈顶的控制器不一定是push入栈的,也有可能是通过-setViewControllers:方法入栈
// 4.具体请看我的“框架知识储备",了解NavigationController的setViewControllers方法
appearingViewController.fd_willAppearInjectBlock = block;
UIViewController *disappearingViewController = self.viewControllers.lastObject;
if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
// 在有新的控制器入栈前,检查栈顶控制器block属性是否有值,如果没有,就赋值
disappearingViewController.fd_willAppearInjectBlock = block;
}
}
- (_FDFullscreenPopGestureRecognizerDelegate *)fd_popGestureRecognizerDelegate
{
// 1.这是我们在1.中第一个提到的类,自定义的pan手势代理,在这个类实现
// 2.由于该类在判断手势是否满足触发条件时,需要根据导航控制器的情况来做判断
// 3.所以将导航控制器交给该类引用(记得用weak,不然会循环引用)
_FDFullscreenPopGestureRecognizerDelegate *delegate = objc_getAssociatedObject(self, _cmd);
if (!delegate) {
delegate = [[_FDFullscreenPopGestureRecognizerDelegate alloc] init];
delegate.navigationController = self;
objc_setAssociatedObject(self, _cmd, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return delegate;
}
- (UIPanGestureRecognizer *)fd_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;
}
- (BOOL)fd_viewControllerBasedNavigationBarAppearanceEnabled
{
// 获取NSNumber对象,注意了,如果NSnumber的value为0的时候,
// if条件也会判断为真,因为NSnumber是对象,对象空的时候为nil而不是0
NSNumber *number = objc_getAssociatedObject(self, _cmd);
if (number) {
// 如果number为0,那么boolValue得到的结果就为NO,反之YES
return number.boolValue;
}
// 代码如果执行到这,说明没设置该属性,默认为YES
self.fd_viewControllerBasedNavigationBarAppearanceEnabled = YES;
return YES;
}
- (void)setFd_viewControllerBasedNavigationBarAppearanceEnabled:(BOOL)enabled
{
// 注意,这里@(enable)是将bool值包装成一个NSNumber类型的对象
SEL key = @selector(fd_viewControllerBasedNavigationBarAppearanceEnabled);
objc_setAssociatedObject(self, key, @(enabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
4.这里添加了框架的功能属性:手势的触发位置、该控制器是否支持手势,导航栏是否隐藏
@implementation UIViewController (FDFullscreenPopGesture)
- (BOOL)fd_interactivePopDisabled
{
// 滑动手势的触发条件,该属性设置为NO,这个控制器将不会触发手势
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setFd_interactivePopDisabled:(BOOL)disabled
{
objc_setAssociatedObject(self, @selector(fd_interactivePopDisabled), @(disabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)fd_prefersNavigationBarHidden
{
// 控制器在viewWillAppear: 及 viewWillDisappear:都会根据该属性决定是否需要隐藏导航栏
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setFd_prefersNavigationBarHidden:(BOOL)hidden
{
objc_setAssociatedObject(self, @selector(fd_prefersNavigationBarHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGFloat)fd_interactivePopMaxAllowedInitialDistanceToLeftEdge
{
// 手势起始点的最大距离,超过该距离,不触发手势
// 64位系统下,CGFLOAT是double类型,32位系统下是float类型
#if CGFLOAT_IS_DOUBLE
return [objc_getAssociatedObject(self, _cmd) doubleValue];
#else
return [objc_getAssociatedObject(self, _cmd) floatValue];
#endif
}
- (void)setFd_interactivePopMaxAllowedInitialDistanceToLeftEdge:(CGFloat)distance
{
// 起点距离,过滤负数
SEL key = @selector(fd_interactivePopMaxAllowedInitialDistanceToLeftEdge);
objc_setAssociatedObject(self, key, @(MAX(0, distance)), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}