FDFullScreenPopGesture源码解析

嘿嘿,花了三天时间把自己的代码从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然后根据一系列的状态判断该手势是否生效。这些状态包括

  1. 当前UINavigationController的栈是否只剩最后一个ViewController了
  2. 当前即将出栈的topViewController是否禁用了fd_interactivePopDisabled,该变量我们稍后会说
  3. 当前手势的启动点是不是离左侧边缘太远了,毕竟我们是要模拟iOS原生的手势操作,原生的不支持全屏,我们为啥要支持!
  4. 当前是否已经处在转场过程中。在这里,可以看到它使用了valueForKey这一Key-Value-Coding技术,它可以访问私有变量_isTransitioning哦!
  5. 方向相反的滑动滚粗。

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];
    }
}

整体来看这段源码,无非做了如下这些事:

  1. 将UIPanGestureRecognizer添加到本来interactivePopGestureRecognizer所在的view上
  2. 这段是重点的重点,一定要往下看!!!

    将PanGesture的target设置为internalTarget,action设置为
    handleNavigationTransition.
  3. 禁用interactivePopGestureRecognizer
  4. 根据是否需要隐藏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>)>

这是什么玩意?让我们分别来看看它答应出来的这些属性。

  1. state = Possible,很简单,就是一个UIGestureRecognizerState = UIGestureRecognizerStatePossible。
  2. view = UILayoutContainerView,不太懂,暂时也没觉得有需要,不管他。
  3. 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指向旧函数即可;而如果没成功,说明这个函数在子类中存在了,我们直接替换也不会影响父类。