FDFullScreenPopGesture源码解析

FDFullScreenPopGesture仅300行不到的代码就完美实现了丝滑的全屏滑动,如果是我们自己实现一个全屏滑动而且还能保证UINavigationBar良好的切换效果你会怎么做呢?

1.最简单的则是在返回的VC中写下如下代码:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.navigationController setNavigationBarHidden:YES animated:animated];
}

当然,如果场景不多还是可取,需要将其隐藏之后,在合适的时机又显示出来,场景一多可能就容易出乱子了

2.直接隐藏系统的UINavigationBar,自己去实现一个View放在顶部,这种就是完全可自定义,不过花费代价也是稍大一些

3.使用截图将前一个界面的视图保存起来,自定义手势,在滑动时判断并显示,这个需要处理的逻辑和情况也相对复杂,因为push和pop操作需要可以pop到上一级或者指定或者根视图控制器等

4.实现UIViewControllerAnimatedTransitioning协议,自定义转场动画,在熟悉的情况下是可行的

5.今天的主角,FDFullScreenPopGesture,完全解耦合,只需要拖入工程中就能实现该效果。那么这么好的东西他是如何实现的呢?下面我们来看一下

工程结构:

  • UINavigationController (FDFullscreenPopGesture)pushViewController:animated的hook

  • UIViewController (FDFullscreenPopGesture):主要进行viewWillAppear的hook

  • _FDFullscreenPopGestureRecognizerDelegate:负责管理UIGestureRecognizerDelegate的代理

  • UIViewController (FDFullscreenPopGesture):通过runtime添加几个设置属性

代码解析:

_FDFullscreenPopGestureRecognizerDelegate

//实现了UIGestureRecognizerDelegate的gestureRecognizerShouldBegin代理方法,对手势的操作进行管理
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    //当navigationController中只有一个ViewController时返回NO,该手势不生效
    if (self.navigationController.viewControllers.count <= 1) {
        return NO;
    }
    
    //当前的 ViewController 禁用了 fd_interactivePopDisabled,fd_interactivePopDisabled是使用objc_getAssociatedObject在类别中添加的一个属性,用户可设置是否禁用
  UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
    if (topViewController.fd_interactivePopDisabled) {
        return NO;
    }
    //同理设置了一个fd_interactivePopMaxAllowedInitialDistanceToLeftEdge属性用于设置最大左边距,当滑动的x坐标大于他时也是无效的
    CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
    CGFloat maxAllowedInitialDistance = topViewController.fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
    if (maxAllowedInitialDistance > 0 && beginningLocation.x > maxAllowedInitialDistance) {
        return NO;
    }

    //当前是否在转场过程中。这里通过 KVC 拿到了 NavigationController 中的私有 _isTransitioning 属性
    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
        return NO;
    }
    
    // 从右往左滑动也是无效的
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
    if (translation.x <= 0) {
        return NO;
    }
    
    return YES;
}

UIViewController (FDFullscreenPopGesturePrivate)

typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);//定义了一个block用于在Swizzling的fd_viewWillAppear方法中回调

//在整个文件被加载到运行时,在 main 函数调用之前被 ObjC 运行时调用的钩子方法
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(fd_viewWillAppear:);
        
        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_addMethod 方法返回 YES 。这时使用 class_getInstanceMethod 函数获取到的 originalSelector 指向的就是父类的方法,我们再通过执行 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 将父类的实现替换到我们自定义的 mrc_viewWillAppear 方法中。这样就达到了在 mrc_viewWillAppear 方法的实现中调用父类实现的目的。
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
          //主类本身有实现需要替换的方法,也就是 class_addMethod 方法返回 NO 。这种情况的处理比较简单,直接交换两个方法的实现就可以了
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)fd_viewWillAppear:(BOOL)animated
{
    // Method Swizzling之后, 调用fd_viewWillAppear:实际执行的代码已经是原来viewWillAppear中的代码了
    [self fd_viewWillAppear:animated];
    
    if (self.fd_willAppearInjectBlock) {
        self.fd_willAppearInjectBlock(self, animated);
    }
}

- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
  //_cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
{
    objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

UINavigationController (FDFullscreenPopGesture)

//将此方法替换了pushViewController:animated:
- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
  
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
        
        // 将自定义的UIPanGestureRecognizer添加到本来interactivePopGestureRecognizer所在的view上
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];

        // 使用kvc拿到内部的targets数组,并找到他的target和action SEL,将fd_fullscreenPopGestureRecognizer的target设置为internalTarget,action设置为handleNavigationTransition.实际上就是将系统的手势事件转发为自定义的手势,触发的事件不变,厉害吧,能找到这些属性也是牛逼炸了
        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_prefersNavigationBarHidden 来显示和隐藏 NavigationBar
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
    
    //调用父类pushViewController:animated
    if (![self.viewControllers containsObject:viewController]) {
        [self fd_pushViewController:viewController animated:animated];
    }
}

- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
{
    //如果设置属性为NO,即为隐藏
    if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
        return;
    }
    
    //viewWillAppear的时候将会调用该方法,实际上内部也是通过调用setNavigationBarHidden:animated:来设置NavigationBar的显示
    __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];
        }
    };
    
    // 将fd_willAppearInjectBlock注入到新的的view controller中
    // 将栈顶的viewController拿出来并且判断是否已经注入了fd_willAppearInjectBlock,没有则添加,因为并不一定每个vc都是通过push加入到栈的,也有可能通过"-setViewControllers:"
    appearingViewController.fd_willAppearInjectBlock = block;
    UIViewController *disappearingViewController = self.viewControllers.lastObject;
    if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
        disappearingViewController.fd_willAppearInjectBlock = block;
    }
}

你可能感兴趣的:(FDFullScreenPopGesture源码解析)