嘿嘿,花了三天时间把自己的代码从700行缩减成了200行,终于有时间可以拜读新源码了,好开心。
好了,废话不多话,今天我们要读了百度出品的一个开源项目FDFullScreenPopGesture。
项目介绍
FDFullScreenPopGesture是一款无需改动即可整合进入现有项目的全局手势操作,使用这个即可以在左侧边缘拖拽的时候返回上一级的效果。
看到这,有人会问,我们这个直接用UIPanGestureRecognizer不也能达到吗?
没错,仅仅是返回上一级这个需求确实很简单。但是iOS7上返回上一级时候,UINavigationBar的切换效果你能实现吗?
而且,我要说的重点是FDFullscreenPopGesture实现思路很赞!!!
源码分析
结构分析
整个FDFullscreenPopGesture其实可以主要拆分成三块:
-- _FDFullscreenPopGestureRecognizerDelegate
-- UIViewController (FDFullscreenPopGesturePrivate)
-- UINavigationController (FDFullscreenPopGesture)
_FDFullscreenPopGestureRecognizerDelegate虽然名字看起来像一个Protocol,但是它实质上是一个NSObject子类,同时实现了UIGestureRecognizerDelegate。这么做的好处是什么呢?不知道大家有没有经历过ViewController重构,以前很多时候,比如我们写UIScrollView,UITableView,他们的Delegate,DataSource都耦合进了ViewController,常常导致MassViewController灾难的发生。单独构建一个专门负责的Delgeate“处理器”是非常有效的手段
UIViewController (FDFullscreenPopGesturePrivate)是一个Category,我们在这个分类里面主要进行viewWillAppear的Hook,具体做什么,后面章节我们细细道来。
UINavigationController (FDFullscreenPopGesture)也是一个分类,是进行pushViewController:animated:方法的hook,实现部分我们后续再看。
源码分析
_FDFullscreenPopGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
// 1.
if (self.navigationController.viewControllers.count <= 1) {
return NO;
}
// 2.
UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
if (topViewController.fd_interactivePopDisabled) {
return NO;
}
// 3.
CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
CGFloat maxAllowedInitialDistance = topViewController.fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) {
return NO;
}
// 4.
if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
return NO;
}
// 5.
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
if (translation.x <= 0) {
return NO;
}
return YES;
}
整个类特别精简,它的职责就是维护一个UINavigationController然后根据一系列的状态判断该手势是否生效。这些状态包括
- 当前UINavigationController的栈是否只剩最后一个ViewController了
- 当前即将出栈的topViewController是否禁用了fd_interactivePopDisabled,该变量我们稍后会说
- 当前手势的启动点是不是离左侧边缘太远了,毕竟我们是要模拟iOS原生的手势操作,原生的不支持全屏,我们为啥要支持!
- 当前是否已经处在转场过程中。在这里,可以看到它使用了valueForKey这一Key-Value-Coding技术,它可以访问私有变量_isTransitioning哦!
- 方向相反的滑动滚粗。
UIViewController (FDFullscreenPopGesturePrivate)
整个这个类也非常简单,就是通过 fd_viewWillAppear hook了 viewWillAppear 这个方法,然后插入了自己一段回调的block。
- (void)fd_viewWillAppear:(BOOL)animated
{
// Forward to primary implementation.
[self fd_viewWillAppear:animated];
if (self.fd_willAppearInjectBlock) {
self.fd_willAppearInjectBlock(self, animated);
}
}
UINavigationController (FDFullscreenPopGesture)
这个分类是整个项目的逻辑控制核心。它干了这么几件事:
- 用 fd_pushViewController:animated: hook pushViewController:animated:
- 禁用UINavgationController的interactivePopGestureRecognizer
- 构建了属于自己UIPanGestureRecognizer替换interactivePopGestureRecognizer,同时把手势的delegate赋值给了_FDFullscreenPopGestureRecognizerDelegate
它的主要核心代码如下:
- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
// 1.
[self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];
// 2.
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];
// 3.
self.interactivePopGestureRecognizer.enabled = NO;
}
// 4.
[self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
// Forward to primary implementation.
if (![self.viewControllers containsObject:viewController]) {
[self fd_pushViewController:viewController animated:animated];
}
}
整体来看这段源码,无非做了如下这些事:
- 将UIPanGestureRecognizer添加到本来interactivePopGestureRecognizer所在的view上
- 这段是重点的重点,一定要往下看!!!
将PanGesture的target设置为internalTarget,action设置为
handleNavigationTransition. - 禁用interactivePopGestureRecognizer
根据是否需要隐藏UINavigationBar来调用fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController进行之前提到过的fdwillAppearInjectBlock设置,代码如下:
- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController { if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) { return; } __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]; } }; // Setup will appear inject block to appearing view controller. // Setup disappearing view controller as well, because not every view controller is added into // stack by pushing, maybe by "-setViewControllers:". appearingViewController.fd_willAppearInjectBlock = block; UIViewController *disappearingViewController = self.viewControllers.lastObject; if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) { disappearingViewController.fd_willAppearInjectBlock = block; } }
这段代码就是将即将消失和展现的ViewController在viewWillAppear设置了一个自定义UINavigationBar的回调,用以根据进入的方式来展现NaviagtionBar,而不会出现突兀的“镂空”。
到这,源码就结束了,可以回家收衣服喽!
重点
源码是不是很简单?有什么好分析的呢?
如果你读到这,哈哈,恭喜啦,重点分析来啦。
首先感谢@J_雨的天才思路,大家可以阅读轻松学习之二——iOS利用Runtime自定义控制器POP手势动画这篇文章,真的很赞
之前我们在上文用红色标注了一段内容:
将PanGesture的target设置为internalTarget,action设置为
handleNavigationTransition。
看起来很容易理解,可是大家有没有想过为什么action的名称是handleNavigationTransition呢?
首先我们先打印看看NavigationController的interactivePopGestureRecognizer究竟是个什么玩意?
<UIScreenEdgePanGestureRecognizer: 0x7fea78ec5950; state = Possible; delaysTouchesBegan = YES; view = <UILayoutContainerView 0x7fea78f77960>; target= <(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7fea78c1c640>)>
这是什么玩意?让我们分别来看看它答应出来的这些属性。
- state = Possible,很简单,就是一个UIGestureRecognizerState = UIGestureRecognizerStatePossible。
- view = UILayoutContainerView,不太懂,暂时也没觉得有需要,不管他。
- target =<(action=handleNavigationTransition:, target=<_uinavigationinteractivetransition 0x7fea78c1c640="">),这个看起来很有用,因为我们都知道,Gesture就是通过Target-Action的方式进行动作触发的。
所以我们赶紧看看这个target是个啥玩意,使用如下命令:
[self.navigationController.interactivePopGestureRecognizer valueForKey:@"target"];
卧槽,一运行,Crash了,报找不到这个Key。咋回事,难道我记错了KVC的用户,赶紧换成valueForKey:@”View”试试。
哎!没错啊!成功得到了如下输出:
-[UILayoutContainerView objectAtIndexedSubscript:]
那咋回事,看来必须祭出屠龙刀Runtime了,嘿嘿,Objective-C面前,一切私有变量都是纸老虎。
unsigned int count = 0;
Ivar *var = class_copyIvarList([UIGestureRecognizer class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar _var = *(var + i);
NSLog(@"%s", ivar_getTypeEncoding(_var));
NSLog(@"%s", ivar_getName(_var));
}
输出太长了,我们找我们想看的,
2015-11-27 02:10:03.873 SamplePhotosApp[85305:2664323] _targets
2015-11-27 02:10:03.873 SamplePhotosApp[85305:2664323] @"NSMutableArray"
卧槽,这丫叫_targets,好吧,赶紧改成valueForKey:@”targets”再试试。
哎,等等,不是_targets吗,怎么能用targets呢?
咳咳,吴老师又要来讲课了!对于KVC来说,它的查找顺序是key -> property -> ivar,也就是说,它会先按照是否有targets这个名称的key,然后targets这个property,最后再找_targets这个ivar。
通过输出log,我们可以发现_targets是个数组,维护了一个个自定义结构维护的target-action配对。
因此,我们现在只要找到这个自定义结构是啥,里面包含了啥就可以了是吧。
当头一棒,很遗憾,苹果太阴了,直接重载了这个自定义结构的debugDescription,特喵的什么都看不到。
事情到了这咋办呢?其实我也没想到,还好上述的参考文章告诉了我们可以依靠断点,通过断点,我们发发现了该自定义结构叫UIGestureRecognizerTarget,我们通过KVC获取其target和action即可。
补充:关于Method Swizzling
Class class = [self class];
SEL originalSelector = @selector(pushViewController:animated:);
SEL swizzledSelector = @selector(fd_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,但是不知道为什么这里需要进行BOOL success = class_addMethod判断。
其主要原因就是如果直接通过method_exchangeImplementations来进行的话,可能子类里并没有originalSelector所代表的方法,你直接和父类进行了交换,这是我们不希望看到的。
因此通过addMethod来判断,如果加成功了,说明原先这个函数在子类中并不存在,我们现在添加了,只要再把swizzleSelector指向旧函数即可;而如果没成功,说明这个函数在子类中存在了,我们直接替换也不会影响父类。