UINavigationBar手勢(shì)側(cè)滑、隱藏bar、UIScrollView側(cè)滑返回研究二

使用過程中有卡頓和測(cè)試失效的問題,還需要繼續(xù)研究?jī)?yōu)化下
上一篇相關(guān)文章:iOS側(cè)滑pop返回的第三方整理研究

知識(shí)點(diǎn):

Runtime+分類+property現(xiàn)實(shí)屬性

前言

當(dāng)在實(shí)際開發(fā)中遇到使用系統(tǒng)navigationBar隱藏或顯示展示某些頁(yè)面,總共有以下4種可能:
顯示導(dǎo)航欄頁(yè)面A->顯示導(dǎo)航欄頁(yè)面B
顯示導(dǎo)航欄頁(yè)面A->隱藏導(dǎo)航欄頁(yè)面B
隱藏導(dǎo)航欄頁(yè)面A->顯示導(dǎo)航欄頁(yè)面B
隱藏導(dǎo)航欄頁(yè)面A->隱藏導(dǎo)航欄頁(yè)面B
在實(shí)際開發(fā)中,經(jīng)常很難同時(shí)處理好這幾種可能,經(jīng)常會(huì)出現(xiàn)導(dǎo)航欄突然閃一下或是進(jìn)入頁(yè)面后才隱藏導(dǎo)航欄,有些在側(cè)滑時(shí)會(huì)導(dǎo)航欄位置是空的或是黑的,顯得特別怪異,但FDFullscreenPopGesture卻很好的處理了這個(gè)難題,現(xiàn)在研究下這個(gè)庫(kù)的實(shí)現(xiàn)

用法

在使用FDFullscreenPopGesture這個(gè)庫(kù)時(shí),在需要隱藏系統(tǒng)導(dǎo)航欄的頁(yè)面的viewDidLoad方法里設(shè)置下fd_prefersNavigationBarHidden屬性,需要顯示導(dǎo)航欄的頁(yè)面什么都不處理,使用起來非常簡(jiǎn)單,如下

// 引入處理側(cè)滑pop返回及處理有無(wú)navbar的庫(kù)
#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文件里寫了一個(gè)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、對(duì)UINavigationController添加一個(gè)分類UINavigationController (FDFullscreenPopGesture),使用Runtime的swizzle黑魔法將pushViewController:animated:的實(shí)現(xiàn)替換,增加上額外的處理fd_pushViewController:animated:
,在這個(gè)增加額外的方法里的主要功能是

  • 2.1、給UINavigationController的interactivePopGestureRecognizer.view添加一個(gè)新的手勢(shì),這個(gè)添加的手勢(shì)代理是寫的另一個(gè)類,同時(shí)讓系統(tǒng)默認(rèn)的處理側(cè)滑pop返回的手勢(shì)注冊(cè)者失效,目的是讓重寫了navigationItem的backItem后也能響應(yīng)側(cè)滑返回
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、設(shè)置當(dāng)前即將要push的ViewController的當(dāng)要處理隱藏導(dǎo)航欄時(shí)的block,這個(gè)方法的邏輯是在push時(shí)給設(shè)置一個(gè)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;

這個(gè)block會(huì)在viewWillAppear:animated:這個(gè)hook的方法里回調(diào),而這個(gè)block的邏輯是根據(jù)fd_prefersNavigationBarHidden來動(dòng)態(tài)隱藏或顯示UINavigationBar,同時(shí)節(jié)將被隱藏的UIViewController如果沒有設(shè)置這個(gè)block,也會(huì)將同樣的邏輯設(shè)置給這個(gè)Controller,保證在UINavigationController的棧里管理的所有UIViewController都有這個(gè)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,當(dāng)前Controller解決不可見時(shí)也會(huì)執(zhí)行一段代碼,代碼邏輯為如果解決要push出來的代碼如果不隱藏導(dǎo)航欄,則設(shè)置[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];
        }
    });
}

總結(jié)

對(duì)代碼進(jìn)行了深入的初步研究后發(fā)現(xiàn),原理是讓每個(gè)Controller的viewWillAppear:animated:方法里都執(zhí)行了一遍是否隱藏導(dǎo)航欄的代碼邏輯,比如我在BaseViewController里定義了一個(gè)lh_hideNavBar熟悉,只要這樣調(diào)用就會(huì)OK,只是FDFullscreenPopGesture使用了分類的方式,另外也添加了更多判斷邏輯的代碼,我的代碼如下

GitHub:TestPopGestureSolution7


吸收了同事的寫法、TZScrollViewPopGesture、FDFullscreenPopGesture后寫了一個(gè)比較簡(jiǎn)單的封裝整理,全部代碼如下(總共112行,包含側(cè)滑、隱藏navbar、UIScrollView側(cè)滑):

UIViewController+LHNavigationGesture.h

#import <UIKit/UIKit.h>
@interface UIViewController (LHNavigationGesture) <UIGestureRecognizerDelegate>
/// 是否隱藏導(dǎo)航欄
@property (nonatomic,assign) BOOL lh_hideNavBar;
/// 給view添加側(cè)滑返回效果
- (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 - ******** 支持手勢(shì)pop側(cè)滑
+ (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的隱藏現(xiàn)實(shí)不突兀
+ (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側(cè)滑滾動(dòng)
- (void)lh_addPopGestureToView:(UIView *)view {
    if (!view) return;
    if (!self.navigationController) {
        // 在控制器轉(zhuǎn)場(chǎng)的時(shí)候,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類型側(cè)滑滾動(dòng)
@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;
    }
    
    // 側(cè)滑手勢(shì)觸發(fā)位置
    CGPoint location = [gestureRecognizer locationInView:self.view];
    CGPoint offSet = [gestureRecognizer translationInView:gestureRecognizer.view];
    BOOL ret = (0 < offSet.x && location.x <= 40);
    return ret;
}

/// 只有當(dāng)系統(tǒng)側(cè)滑手勢(shì)失敗了,才去觸發(fā)ScrollView的滑動(dòng)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

@end
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容