FDFullscreenPopGesture全屏手勢返回源碼結構及解析

內容均為原創, 如有任何疑問或者錯誤,請在文章下留言或者直接與我聯系,一定及時回復: )
· 前言
· 框架背景
· 框架知識準備
· 源碼解析
· 框架工作流程圖

· 前言

由于6Plus的出現,iphone的默認導航欄又是在屏幕頂部,對于app的返回操作大屏手機對于小手的用戶來說操作顯得不那么友好。iOS7為了提升app的返回體驗,增加了邊緣側滑返回手勢,但是對于小手用戶來說,返回體驗沒有徹底得到改善。于是開發者們開始絞盡腦汁地想各種辦法,其中一種辦法,也就是今天要講的---將返回手勢變為全屏的框架。
github千星框架 FDFullscreenPopGesture
這個框架能get到4K+的星星,肯定是有過人之處的。抱著學習的態度,去看了下源碼,對于runtime以及封裝代碼的思路,都有很大的幫助。前言就到這里,下面我會盡量清晰的表達我看源碼的思路,盡量把一些知識點說的通俗易懂。


· 框架背景

這個擴展來自 @J_雨 同學的這個很天才的思路,他的文章地址:戳這里

如果不太愿意戳進去看,我這里來總結下這位同學的思路:

1.如果自定義手勢的話,還要考慮控制器切換的動畫,成本太高太麻煩。
2.iOS7.0之后蘋果提供了邊緣滑動手勢,獲取到這個邊緣滑動手勢,然后把它的觸發范圍從邊緣改成全屏不就好了?然后經過一番折騰,發現沒有辦法修改,這個方法是不可行的。
3.那么既然不能將它的手勢觸發的方法找到,我們自己去創建一個全屏手勢,去調用那個方法,不就好了?bingo,可行。
4.有人對這個思路進行了封裝于是有了FDFullscreenPopGesture


· 框架知識準備

· 為分類添加屬性

objc_setAssociatedObject objc_getAssociatedObject

傳送門:Runtime奇技淫巧之objc_setAssociatedObject,objc_getAssociatedObject

· 在 main 函數調用之前被 ObjC 運行時調用的方法,框架只需要放入項目文件夾即可實現框架功能,都是這個鉤子方法的功勞。

+(void)load
傳送門:Objective-C Method Swizzling 的最佳實踐

· NavigationController可以通過調用setViewController方法將畫面的跳轉歷史路徑(堆棧)完全替換

傳送門:頁面的跳轉技巧--setViewControllers


· 框架源碼解析

打開項目我們能看到,該框架只有一個.h 和.m文件。
.h中只暴露了UINavigationController 和 UIViewController的兩個分類屬性。
.m中包括四部分

@implementation _FDFullscreenPopGestureRecognizerDelegate : NSObject (私有)
@implementation UIViewController (FDFullscreenPopGesturePrivate)(私有)
@implementation UINavigationController (FDFullscreenPopGesture)
@implementation UIViewController (FDFullscreenPopGesture)
· 下面為大家一一講解下這四個implementation都干了一些什么事

1.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
  1. @interface UIViewController (FDFullscreenPopGesturePrivate) 定義了一個block,在ViewControllerWillAppear的時候會被注入
    @implementation UIViewController (FDFullscreenPopGesturePrivate)在main函數之前會將系統的viewWillAppear方法和viewWillDisappear方法替換成分類中的方法。
typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);

@interface UIViewController (FDFullscreenPopGesturePrivate)

@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;

@end

const NSString *block = @"block";

@implementation UIViewController (FDFullscreenPopGesturePrivate)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       // 獲取系統的viewWillAppear:方法
        Method viewWillAppear_originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
       // 獲取自定義的fd_viewWillAppear:方法 
        Method viewWillAppear_swizzledMethod = class_getInstanceMethod(self, @selector(fd_viewWillAppear:));
       // 兩者交換
        method_exchangeImplementations(viewWillAppear_originalMethod, viewWillAppear_swizzledMethod);
    
       // 獲取系統的viewWillDisappear:方法 
        Method viewWillDisappear_originalMethod = class_getInstanceMethod(self, @selector(viewWillDisappear:));
       // 獲取自定義的viewWillDisappear:方法 
        Method viewWillDisappear_swizzledMethod = class_getInstanceMethod(self, @selector(fd_viewWillDisappear:));
       // 兩者交換method_exchangeImplementations(viewWillDisappear_originalMethod, viewWillDisappear_swizzledMethod);
    });
}

- (void)fd_viewWillAppear:(BOOL)animated
{
    // 1.為了不破壞原本的業務邏輯,先執行原來的viewWillAppear方法
    // 2.為什么在fd_viewWillAppear:方法中調用fd_viewWillAppear:方法不會引起死循環?
    // 3.因為fd_viewWillAppear:這個方法已經和viewWillAppear:方法做了交換
    // 4.所以調用 fd_viewWillAppear:方法相當于調用了viewWillAppear:方法,
    // 5.調用viewWillAppear:方法相當于調用了fd_viewWillAppear:方法
    [self fd_viewWillAppear:animated];
    
    // 執行注入的block 這個block到底干了什么事情,會在后面講到
    if (self.fd_willAppearInjectBlock) {
        self.fd_willAppearInjectBlock(self, animated);
    }
}

- (void)fd_viewWillDisappear:(BOOL)animated
{
    // 同理,這里不再贅述。
    [self fd_viewWillDisappear:animated];
    // 1.當用戶有pop或者push操作的時候,
    // 2.根據導航欄棧頂控制的fd_prefersNavigationBarHidden這個分類屬性,
    // 3.控制導航欄是否需要隱藏
    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];
        }
    });
}

- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
  // 1. 調用fd_willAppearInjectBlock屬性的get方法的時候
  // 2. 會在本類中以該get方法的名稱為key,找到對應的value,也就是該block的值
  // 3. 更多關于runtime為分類添加屬性的知識,請看框架知識儲備
    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);
}
@end

3.全屏手勢的核心返回功能在此實現,交換push方法后,將系統返回手勢替換為自定義手勢,設置代理,如果允許用戶根據控制器的分類屬性控制導航欄顯示或者隱藏,則給入棧的控制器的block賦值。

@implementation UINavigationController (FDFullscreenPopGesture)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // 同上,為了注入代碼,替換導航控制器的push方法。
        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.打個比方,如果我創建了一個testNavgationController繼承自UINavigationController
        // 4.但是,我自定義的testNavgationController只進行了修改navBar背景色的方法
        // 5.自定義的testNavgationController并沒有重寫 pushViewController:animated: 
        // 6.這個時候,如果我直接調用方法交換的話,會和父類中的pushViewController:animated:交換
        // 7.顯然,這不是我們想要的結果,我們只希望和testNavigation的pushViewController:animated:方法交換
        // 8.所以先調用class_addMethod方法,檢查本類是否實現了pushViewController:animated: 
        // 9.如果實現了,那很好,直接交換
        // 10.如果沒實現,那么class_addMethod已經把push方法 (對應的實現是fd_push)添加到了本類
        // 11.我們只需要再調用class_replaceMethod方法添加fd_push(對應的實現是push) 添加到本類
        // 12.這樣,就達到了方法交換的目的
        // 13.pushViewController:animated: 的內部實現為fd_pushViewController:animated: 
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            // 1. class_replaceMethod方法會檢查是否存在swizzledSelector這個方法名
            // 2.如果存在,那么將原來實現替換為 originalMethod
            // 3.如果不存在 則會先 添加這個方法名swizzledSelector,
            // 4.然后再添加這個方法的實現originalMethod
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            NSLog(@"%s",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];
    }
}

- (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;
}

- (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);
}
@end

4.這里添加了框架的功能屬性:手勢的觸發位置、該控制器是否支持手勢,導航欄是否隱藏

@implementation UIViewController (FDFullscreenPopGesture)

- (BOOL)fd_interactivePopDisabled
{
   // 滑動手勢的觸發條件,該屬性設置為NO,這個控制器將不會觸發手勢
    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
{
    // 控制器在viewWillAppear: 及 viewWillDisappear:都會根據該屬性決定是否需要隱藏導航欄
    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
{
// 手勢起始點的最大距離,超過該距離,不觸發手勢
// 64位系統下,CGFLOAT是double類型,32位系統下是float類型
#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);
}

· FDFullscreenPopGesture工作流程圖

工作流程圖.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,229評論 4 61
  • 鄰居楊君,姑蘇人士,儒雅謙恭又年輕有為,聽說是單位里領導重點培養的青年骨干,家有賢妻,和美幸福得令人妒忌,因性情相...
    山陰路閱讀 273評論 0 1
  • 昨晚,同事突然問我:你說到底嫁一個怎樣的人才靠譜?我隨即便問:你又猶豫了。她輕輕的點頭,嘴角微翹:“嗯”。 我說,...
    愛讀分享閱讀 476評論 0 0
  • 可以隨時記錄想法嗎
    華萌玫瑰王斌松閱讀 111評論 0 0
  • 公司小王離職了,辦完離職的所有手續,和辦公室的人握手說再見,抱著自己的個人物品走出了辦公室,如同窗外飄落的秋葉,形...
    行者靜思錄閱讀 610評論 0 0