美文网首页好东西小知识点
UINavigationBar手势侧滑、隐藏bar、UIScro

UINavigationBar手势侧滑、隐藏bar、UIScro

作者: 独孤流 | 来源:发表于2017-08-29 17:47 被阅读836次

    使用过程中有卡顿和测试失效的问题,还需要继续研究优化下
    上一篇相关文章:iOS侧滑pop返回的第三方整理研究

    知识点:

    Runtime+分类+property现实属性

    前言

    当在实际开发中遇到使用系统navigationBar隐藏或显示展示某些页面,总共有以下4种可能:
    显示导航栏页面A->显示导航栏页面B
    显示导航栏页面A->隐藏导航栏页面B
    隐藏导航栏页面A->显示导航栏页面B
    隐藏导航栏页面A->隐藏导航栏页面B
    在实际开发中,经常很难同时处理好这几种可能,经常会出现导航栏突然闪一下或是进入页面后才隐藏导航栏,有些在侧滑时会导航栏位置是空的或是黑的,显得特别怪异,但FDFullscreenPopGesture却很好的处理了这个难题,现在研究下这个库的实现

    用法

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

    // 引入处理侧滑pop返回及处理有无navbar的库
    #import "UINavigationController+FDFullscreenPopGesture.h"
    @interface HomeController ()
    @end
    
    @implementation HomeController
    #pragma mark - life cycle
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.fd_prefersNavigationBarHidden = YES;
    }
    @end
    

    原理研究

    1、在UINavigationController+FDFullscreenPopGesture文件里写了一个UIViewController的分类UINavigationController+FDFullscreenPopGesture,并利用property和Runtime的方式给UIViewController添加fd_prefersNavigationBarHidden属性

    @interface UIViewController (FDFullscreenPopGesture)
    @property (nonatomic, assign) BOOL fd_prefersNavigationBarHidden;
    @end
    
    @implementation UIViewController (FDFullscreenPopGesture)
    
    - (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);
    }
    
    @end
    

    2、对UINavigationController添加一个分类UINavigationController (FDFullscreenPopGesture),使用Runtime的swizzle黑魔法将pushViewController:animated:的实现替换,增加上额外的处理fd_pushViewController:animated:
    ,在这个增加额外的方法里的主要功能是

    • 2.1、给UINavigationController的interactivePopGestureRecognizer.view添加一个新的手势,这个添加的手势代理是写的另一个类,同时让系统默认的处理侧滑pop返回的手势注册者失效,目的是让重写了navigationItem的backItem后也能响应侧滑返回
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
            
            // Add our own gesture recognizer to where the onboard screen edge pan gesture recognizer is attached to.
            [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];
            
            // Forward the gesture events to the private handler of the onboard gesture recognizer.
            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];
            
            // Disable the onboard gesture recognizer.
            self.interactivePopGestureRecognizer.enabled = NO;
        }
    
    • 2.2、设置当前即将要push的ViewController的当要处理隐藏导航栏时的block,这个方法的逻辑是在push时给设置一个block,如下
    __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];
            }
        };
        appearingViewController.fd_willAppearInjectBlock = block;
    

    这个block会在viewWillAppear:animated:这个hook的方法里回调,而这个block的逻辑是根据fd_prefersNavigationBarHidden来动态隐藏或显示UINavigationBar,同时节将被隐藏的UIViewController如果没有设置这个block,也会将同样的逻辑设置给这个Controller,保证在UINavigationController的栈里管理的所有UIViewController都有这个block,全部代码如下:

    - (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;
        }
    }
    
    • 2.3、在UIViewController即将push出新的Controller,当前Controller解决不可见时也会执行一段代码,代码逻辑为如果解决要push出来的代码如果不隐藏导航栏,则设置[self.navigationController setNavigationBarHidden:NO animated:NO]
      全部代码如下:
    - (void)fd_viewWillDisappear:(BOOL)animated
    {
        // Forward to primary implementation.
        [self fd_viewWillDisappear:animated];
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            UIViewController *viewController = self.navigationController.viewControllers.lastObject;
            if (viewController && !viewController.fd_prefersNavigationBarHidden) {
                [self.navigationController setNavigationBarHidden:NO animated:NO];
            }
        });
    }
    

    总结

    对代码进行了深入的初步研究后发现,原理是让每个Controller的viewWillAppear:animated:方法里都执行了一遍是否隐藏导航栏的代码逻辑,比如我在BaseViewController里定义了一个lh_hideNavBar熟悉,只要这样调用就会OK,只是FDFullscreenPopGesture使用了分类的方式,另外也添加了更多判断逻辑的代码,我的代码如下

    GitHub:TestPopGestureSolution7


    吸收了同事的写法、TZScrollViewPopGestureFDFullscreenPopGesture后写了一个比较简单的封装整理,全部代码如下(总共112行,包含侧滑、隐藏navbar、UIScrollView侧滑):

    UIViewController+LHNavigationGesture.h

    #import <UIKit/UIKit.h>
    @interface UIViewController (LHNavigationGesture) <UIGestureRecognizerDelegate>
    /// 是否隐藏导航栏
    @property (nonatomic,assign) BOOL lh_hideNavBar;
    /// 给view添加侧滑返回效果
    - (void)lh_addPopGestureToView:(UIView *)view;
    @end
    

    UIViewController+LHNavigationGesture.m

    #import "UIViewController+LHNavigationGesture.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (LHNavigationGesture)
    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            [self swizzleBarHidden];
            [self swizzlePopGesture];
        });
    }
    #pragma mark - ******** 支持手势pop侧滑
    + (void)swizzlePopGesture
    {
        Method viewDidLoad_originalMethod = class_getInstanceMethod(self, @selector(viewDidLoad));
        Method viewDidLoad_swizzledMethod = class_getInstanceMethod(self, @selector(lh_viewDidLoad));
        method_exchangeImplementations(viewDidLoad_originalMethod, viewDidLoad_swizzledMethod);
    }
    - (void)lh_viewDidLoad
    {
        [self lh_viewDidLoad];
        self.navigationController.interactivePopGestureRecognizer.delegate = (id)self;
    }
    #pragma mark - ******** 支持navigationBar的隐藏现实不突兀
    + (void)swizzleBarHidden
    {
        Method viewWillAppear_originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
        Method viewWillAppear_swizzledMethod = class_getInstanceMethod(self, @selector(lh_viewWillAppear:));
        method_exchangeImplementations(viewWillAppear_originalMethod, viewWillAppear_swizzledMethod);
    }
    - (void)lh_viewWillAppear:(BOOL)animated
    {
        [self lh_viewWillAppear:animated];
        
        [self.navigationController setNavigationBarHidden:self.lh_hideNavBar animated:animated];
    }
    
    - (void)setLh_hideNavBar:(BOOL)lh_hideNavBar
    {
        objc_setAssociatedObject(self, @selector(lh_hideNavBar), @(lh_hideNavBar), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (BOOL)lh_hideNavBar
    {
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    #pragma mark - ******** 支持UIScrollView侧滑滚动
    - (void)lh_addPopGestureToView:(UIView *)view {
        if (!view) return;
        if (!self.navigationController) {
            // 在控制器转场的时候,self.navigationController可能是nil,这里用GCD和递归来处理这种情况
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self lh_addPopGestureToView:view];
            });
        } else {
            UIPanGestureRecognizer *pan = self.lh_popGestureRecognizer;
            if (![view.gestureRecognizers containsObject:pan]) {
                [view addGestureRecognizer:pan];
            }
        }
    }
    
    - (UIPanGestureRecognizer *)lh_popGestureRecognizer {
        UIPanGestureRecognizer *pan = objc_getAssociatedObject(self, _cmd);
        if (!pan) {
            
            NSArray *internalTargets = [self.navigationController.interactivePopGestureRecognizer valueForKey:@"targets"];
            id target = [internalTargets.firstObject valueForKey:@"target"];
            SEL action = NSSelectorFromString(@"handleNavigationTransition:");
            pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:action];
            pan.maximumNumberOfTouches = 1;
            pan.delegate = self.navigationController;
            self.navigationController.interactivePopGestureRecognizer.enabled = NO;
            objc_setAssociatedObject(self, _cmd, pan, OBJC_ASSOCIATION_ASSIGN);
        }
        return pan;
    }
    @end
    
    #pragma mark  ******** 支持UIScrollView类型侧滑滚动
    @interface UINavigationController (LHPopGesturePrivate)
    @end
    
    @implementation UINavigationController (LHPopGesture)
    
    - (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer {
        if ([[self valueForKey:@"_isTransitioning"] boolValue]) {
            return NO;
        }
        if ([self.navigationController.transitionCoordinator isAnimated]) {
            return NO;
        }
        if (self.childViewControllers.count <= 1) {
            return NO;
        }
        
        // 侧滑手势触发位置
        CGPoint location = [gestureRecognizer locationInView:self.view];
        CGPoint offSet = [gestureRecognizer translationInView:gestureRecognizer.view];
        BOOL ret = (0 < offSet.x && location.x <= 40);
        return ret;
    }
    
    /// 只有当系统侧滑手势失败了,才去触发ScrollView的滑动
    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
        return YES;
    }
    
    @end
    

    相关文章

      网友评论

        本文标题:UINavigationBar手势侧滑、隐藏bar、UIScro

        本文链接:https://www.haomeiwen.com/subject/yloodxtx.html