FDFullscreenPopGesture源码学习

首先附上项目地址:
FDFullscreenPopGesture
本篇使用1.1版本源码进行学习

此项目以AOP的方式实现了UINavigationController的“一行代码实现全屏滑动返回”功能。其主要功能是通过UINavigationController内置的interactivePopGestureRecognizer手势对象。通过使用自定义的UIPanGestureRecognizer类实例绑定原手势的actionSEL,使响应范围扩大到整个屏幕,巧妙地实现了此功能。详细解释可以查看原博客轻松学习之二——iOS利用Runtime自定义控制器POP手势动画或本人的读后解析UINavigationController的全屏拖动返回

1. 主要结构说明

整个项目非常简单,对外只有一组.h及.m文件。代码量也只有区区两三百行,足够轻量且高效,值得我等膜拜学习~

1.1 头文件的结构说明

项目对外公开的类及主要功能为:

  • UINavigationController的扩展类:UINavigationController + FDFullscreenPopGesture
名称 类型 说明
fd_fullscreenPopGestureRecognizer 属性 真正的全屏拖动响应的手势对象
fd_viewControllerBasedNavigationBarAppearanceEnabled 属性 是否允许ViewController对象自定义导航栏外观(默认为YES)
  • UIViewController的扩展类:UIViewController + FDFullscreenPopGesture

主要用于ViewController对象自主控制全屏手势的使能(以便ViewController的视图也处理自定义的拖动手势,防止手势冲突)

名称 类型 说明
fd_interactivePopDisabled 属性 是否允许响应UINavigationController的全屏拖动手势
fd_prefersNavigationBarHidden 属性 表名当前ViewController对象的导航栏是否隐藏(默认为NO)
1.2 实现文件的结构说明
  • 遵循UIGestureRecognizerDelegate协议的类:_FDFullscreenPopGestureRecognizerDelegate
名称 类型 说明
navigationController weak属性 内部需要根据导航器对象做处理

内部实现了gestureRecognizerShouldBegin:方法,用于处理不同情况下是否响应拖动手势。
设计此类的主要目的是将此功能独立出来,作为UINavigationController + FDFullscreenPopGesture的工具类使用,简化了原类代码,使结构更加清晰。

  • UIViewController的私有扩展类:UIViewController + FDFullscreenPopGesturePrivate
名称 类型 说明
fd_willAppearInjectBlock block对象,属性 保存注入的block对象到堆内存,等待合适时机执行【按命名来说,在ViewWillAppear方法中执行】

在运行时交换了ViewWillAppear:方法实现,在其中注入执行了block对象。

  • UINavigationController + FDFullscreenPopGesture的实现

交换了pushViewController:animated:方法实现,在内部实现了主要功能:

  1. 使用自定义的UIPanGestureRecognizer对象绑定了原有手势的actionSEL,添加到UINavigationController的切换控制视图上。
  2. 声明并实现了注入的_FDViewControllerWillAppearInjectBlock对象,并赋值给指定的UIViewController对象。
  • UIViewController + FDFullscreenPopGesture的实现

只是对扩展中声明的属性进行实现。

2. 使用Category扩展原类,并向指定方法注入新的功能

在OC中,一般是通过Category对Class进行扩展的方式来实现AOP的。本项目即是如此。例如,通过对UINavigationController进行扩展,在load时对方法进行替换,将手势添加到导航器控制视图上,并绑定上原来的targetSEL

+ (void)load
{
    // 交换push方法实现,用于执行自定义操作
    Method originalMethod = class_getInstanceMethod(self, @selector(pushViewController:animated:));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_pushViewController:animated:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    // 检查导航器的切换视图中的手势数组中,是否已经包含全屏拖动手势对象
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
        // 没有,则向其中添加自定义的拖拽手势
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];

        // 通过查看运行时期间,手势对象内部的存储结构,获取原手势响应的target和执行的SEL
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        // 设置自定义的拖拽手势代理,处理不同情况下手势是否响应(故上面的类用于解耦手势的代理方法实现,简化本类代码)
        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
        // 将原手势的action和SEL绑定到自定义的拖拽手势中
        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];

        // 禁止原手势响应(原手势为边缘拖动手势)
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    
    // 处理自定义导航栏外观
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
    
    [self fd_pushViewController:viewController animated:animated];
}

如此实现,即可在引入头文件后,对应的UINavigationController实例自动添加全屏返回手势,对原类的耦合性降至最低。

本项目中,UIViewController的扩展也使用了此方式,以达到在ViewWillAppear:方法中执行注入block,实现运行时对导航栏隐藏设置的修改。充分利用了OC语言的动态性,使代码简洁易用,降低耦合。

3. 防止block对self捕获产生的引用循环,并保证block执行时的完整性

fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:方法中,作者初始化了UIViewController对象在ViewWillAppear:方法中需要注入的block。先看代码:

- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
{
    // 检查ViewController对象是否允许自定义导航栏外观
    if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
        return;
    }
    
    // 声明self弱指针
    __weak typeof(self) weakSelf = self;
    // 声明并实现注入的block对象
    _FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) {
        // block内部捕获的是self的弱指针,不会对self进行强引用
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            // 根据外部设置,隐藏或显示当前ViewController下的导航栏
            [strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated];
        }
    };
    
    // 将block对象赋值给扩展后的ViewController对象(适时调用)
    appearingViewController.fd_willAppearInjectBlock = block;
    // 获取导航栈的最后一个ViewController(即当前已显示的,马上要被新的ViewController压到栈中的)
    UIViewController *disappearingViewController = self.viewControllers.lastObject;
    if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
        // 即将消失的ViewController若存在(即新push的ViewController不是根视图的情况),也将block赋值给它(以备pop时,前一个ViewController对象也能执行block,viewWillAppear时修改导航栏外观)
        disappearingViewController.fd_willAppearInjectBlock = block;
    }
}

其中,最为重要的是:

// block外部声明
__weak typeof(self) weakSelf = self;

// block函数体内部声明
 __strong typeof(weakSelf) strongSelf = weakSelf;

我们都知道,使用__weak修饰的OC对象,可以防止__NSMallocBlock对象在捕获时对其添加强引用(详见《Objective-C高级编程 iOS与OS X多线程与内存管理》16),避免产生引用循环。但是这样有一个弊端,就是假设当self释放后,若block在此时执行,self对象自动置为nil,预定的功能可能就无法正常实现了。为了防止这种情况发生,可以声明一个__strong修饰的局部指针变量,指向捕获的弱对象,既可以保证在block执行过程中self不被释放,还可以避免对self进行强引用(__strong变量会在作用域外被自动release)

4. 总结

  • FDFullscreenPopGesture主要通过Category的方式对UINavigationController和UIViewController进行功能扩展,避免了Class继承方式的“笨重”,简化了引入和使用。
  • 在Category的load方法中,替换了原类的方法实现(原类的load加载要在扩展类之前),在其中注入了指定功能。
  • 使用KVC的方式,巧妙地进行UIGestureRecognizer对象的替换,近乎“无损”地实现了全屏滑动返回功能,避免了使用传统的交互式动画方式实现自定义的UINavigationController。
  • 使用__weak对象避免block的引用循环,并用__strong对象保证block执行过程中捕获对象不被释放。

你可能感兴趣的:(FDFullscreenPopGesture源码学习)