阅读 659

iOS-FDFullscreenPopGesture详解

原文链接: www.jianshu.com
一 序言

由于Plus的出现,iphone的默认导航栏又是在屏幕顶部,对于app的返回操作大屏手机对于小手的用户来说操作显得不那么友好。iOS7为了提升app的返回体验,增加了边缘侧滑返回手势,但是对于小手用户来说,返回体验没有彻底得到改善。于是开发者们开始绞尽脑汁地想各种办法,其中一种办法,也就是今天要讲主角-将返回手势变为全屏侧滑返回的框架

二 剖析这个框架需要了解的知识点
  • 为分类添加属性 objc_setAssociatedObject,objc_getAssociatedObject
  • +(void)load方法执行的时机 ,在 main 函数调用之前被 ObjC 运行时会将所有类加载进内存,会调用每一个类的load方法。
  • NavigationController可以通过调用setViewController方法将画面的跳转历史路径(堆栈)完全替换

框架源码解析

1 文件目录结构

打开项目我们能看到,该框架只有一个.h 和.m文件。

  • .h中只暴露了UINavigationController 和 UIViewController的两个分类属性。
@interface UINavigationController (FDFullscreenPopGesture)
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *fd_fullscreenPopGestureRecognizer;
@property (nonatomic, strong, readonly) UIScreenEdgePanGestureRecognizer *fd_rtlFullscreenPopGestureRecognizer;
@property (nonatomic, assign) BOOL fd_viewControllerBasedNavigationBarAppearanceEnabled;
@end

@interface UIViewController (FDFullscreenPopGesture)
@property (nonatomic, assign) BOOL fd_interactivePopDisabled;
@property (nonatomic, assign) BOOL fd_prefersNavigationBarHidden;
@property (nonatomic, assign) CGFloat fd_interactivePopMaxAllowedInitialDistanceToLeftEdge;
@end复制代码
  • .m中包括四部分
@implementation _FDFullscreenPopGestureRecognizerDelegate : NSObject (私有)
@implementation UIViewController (FDFullscreenPopGesturePrivate)(私有)
@implementation UINavigationController (FDFullscreenPopGesture)
@implementation UIViewController (FDFullscreenPopGesture)复制代码
  • 下面为大家一一讲解下这四个implementation都干了一些什么事
1. _FDFullscreenPopGestureRecognizerDelegate

_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复制代码
2.UIViewController (FDFullscreenPopGesturePrivate)
  1. 在该分类中定义了一个block,并且在viewWillAppear:的时候被注入
  2. main函数之前,交换系统的两个实现方法viewWillAppear:viewWillDisappear:

具体代码如下

typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);

@interface UIViewController (FDFullscreenPopGesturePrivate)
@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;
@end

@implementation UIViewController (FDFullscreenPopGesturePrivate)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        //viewWillAppear
        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 {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
        //viewWillDisappear
        SEL originalSelector2 = @selector(viewWillDisappear:);
        SEL swizzledSelector2 = @selector(fd_viewWillDisappear:);
        
        Method originalMethod2 = class_getInstanceMethod(class, originalSelector2);
        Method swizzledMethod2 = class_getInstanceMethod(class, swizzledSelector2);
        
        BOOL success2 = class_addMethod(class, originalSelector2, method_getImplementation(swizzledMethod2), method_getTypeEncoding(swizzledMethod2));
        if (success2) {
            class_replaceMethod(class, swizzledSelector2, method_getImplementation(originalMethod2), method_getTypeEncoding(originalMethod2));
        } else {
            method_exchangeImplementations(originalMethod2, swizzledMethod2);
        }
    });
}复制代码

交换方法后做的事情

- (void)fd_viewWillAppear:(BOOL)animated {
    // Forward to primary implementation.
    // 为了不破坏原本的业务逻辑,先执行原来的viewWillAppear方法
    [self fd_viewWillAppear:animated];
    
    // 执行注入的block 这个block到底干了什么事情,会在后面讲到
    if (self.fd_willAppearInjectBlock) {
        self.fd_willAppearInjectBlock(self, animated);
    }
    
    //设置导航的显示/隐藏
    // 根据导航栏栈顶控制的fd_prefersNavigationBarHidden这个分类属性,- 控制导航栏是否需要隐藏
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(CGFLOAT_MIN * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        UIViewController *vc = [self.navigationController.viewControllers lastObject];
        if (vc.fd_prefersNavigationBarHidden) {
            [self.navigationController setNavigationBarHidden:YES animated:NO];
        } else {
            [self.navigationController setNavigationBarHidden:NO animated:NO];
        }
    });
}

- (void)fd_viewWillDisappear:(BOOL)animated{
    
    [self fd_viewWillDisappear:animated];
    
    //设置导航的显示/隐藏
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(CGFLOAT_MIN * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        UIViewController *vc = [self.navigationController.viewControllers lastObject];
        if (vc.fd_prefersNavigationBarHidden) {
            [self.navigationController setNavigationBarHidden:YES animated:NO];
        } else {
            [self.navigationController setNavigationBarHidden:NO animated:NO];
        }
    });
}复制代码

block的使用

- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
    // 1. 调用fd_willAppearInjectBlock属性的get方法的时候
    // 2. 会在本类中以该get方法的名称为key,找到对应的value,也就是该block的值
    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);
}复制代码
3.UINavigationController (FDFullscreenPopGesture)

全屏手势的核心返回功能在此实现,交换push方法后,将系统返回手势替换为自定义手势,设置代理,如果允许用户根据控制器的分类属性控制导航栏显示或者隐藏,则给入栈的控制器的block赋值。

  • 交换方法实现
+ (void)load {
    // Inject "-pushViewController:animated:"
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        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);
        
        // 1.这里要注意,class_addMethod是为了检查本类是否实现了这个方法
        // 2.如果方法添加成功,代表本类没有实现该方法(该方法在父类中实现,却没有在子类中实现)
        // 3.如果实现了,那很好,直接交换
        // 4.如果没实现,那么class_addMethod已经把push方法 (对应的实现是fd_push)添加到了本类
        // 5.我们只需要再调用class_replaceMethod方法添加fd_push(对应的实现是push) 添加到本类
        // 6.这样,就达到了方法交换的目的
        // 7.pushViewController:animated: 的内部实现为fd_pushViewController:animated:
        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);
        }
    });
}复制代码
  • 添加手势
- (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];
    }
}复制代码
  • 设置当前即将要push的ViewController的当要处理隐藏导航栏时的block
- (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;
}复制代码
  • A view controller is able to control navigation bar's appearance by itself
- (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);
}复制代码
4. UIViewController (FDFullscreenPopGesture)

这里添加了框架的功能属性:手势的触发位置、该控制器是否支持手势,导航栏是否隐藏

@implementation UIViewController (FDFullscreenPopGesture)

- (BOOL)fd_interactivePopDisabled
{
    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
{
    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
{
#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);
}

@end复制代码
三 FDFullscreenPopGesture工作流程图

下面参考别人绘制的一张流程图

image.png
四 使用

在使用FDFullscreenPopGesture时,在需要隐藏系统导航栏的页面的viewDidLoad方法里设置下fd_prefersNavigationBarHiddenYES属性,需要显示导航栏的页面什么都不处理,使用起来非常简单。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.fd_prefersNavigationBarHidden = YES;
}复制代码

本文参考FDFullscreenPopGesture全屏手势返回源码结构及解析,非常感谢该作者.


  • 如有错误,欢迎指正,多多点赞,打赏更佳,您的支持是我写作的动力。

项目连接地址 - FDFullScreenPopGestureDemo