App開發流程之右滑返回手勢功能

来源:http://www.cnblogs.com/ALongWay/archive/2016/09/21/5893515.html
-Advertisement-
Play Games

iOS7以後,導航控制器,自帶了從屏幕左邊緣右滑返回的手勢功能。 但是,如果自定義了導航欄返回按鈕,這項功能就失效了,需要自行實現。又如果需要修改手勢觸發範圍,還是需要自行實現。 廣泛應用的一種實現方案是,採用私有變數和Api,完成手勢交互和返回功能,自定義手勢觸發條件和額外功能。 另一種實現方案是 ...


iOS7以後,導航控制器,自帶了從屏幕左邊緣右滑返回的手勢功能。

但是,如果自定義了導航欄返回按鈕,這項功能就失效了,需要自行實現。又如果需要修改手勢觸發範圍,還是需要自行實現。

廣泛應用的一種實現方案是,採用私有變數和Api,完成手勢交互和返回功能,自定義手勢觸發條件和額外功能。

另一種實現方案是,採用UINavigationController的代理方法實現交互和動畫:

- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController

                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);

 

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController

                                   animationControllerForOperation:(UINavigationControllerOperation)operation

                                                fromViewController:(UIViewController *)fromVC

                                                  toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);

前者,特點是便捷,但是只能使用系統定義的交互和動畫;後者,特點是高度自定義,但是需要額外實現交互協議和動畫協議。

 

採用私有變數和Api,實現右滑返回手勢功能。

先看最核心的邏輯:

- (void)base_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.base_panGestureRecognizer]) {
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.base_panGestureRecognizer];
        
        //使用KVC獲取私有變數和Api,實現系統原生的pop手勢效果
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        self.base_panGestureRecognizer.delegate = [self base_panGestureRecognizerDelegateObject];
        [self.base_panGestureRecognizer addTarget:internalTarget action:internalAction];
        
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    
    [self base_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
    
    if (![self.viewControllers containsObject:viewController]) {
        [self base_pushViewController:viewController animated:animated];
    }
}

 

UINavigationController的interactivePopGestureRecognizer屬性,是系統專用於將viewController彈出導航棧的手勢識別對象,有一個私有變數名為“targets”,類似為NSArray;該數組第一個元素對象,有一個私有變數名為“target”,即為實現預期交互的對象;該對象有一個私有方法名為“handleNavigationTransition:”,即為目標方法。

在返回手勢交互的UIView(self.interactivePopGestureRecognizer.view)上添加一個自定義的UIPanGestureRecognizer,利用其delegate對象實現的代理方法gestureRecognizerShouldBegin來控制手勢生效條件。最後禁用系統的返回手勢識別對象,就可以用自定義實現的pan手勢來調用系統的pop交互和動畫。

判斷pan手勢是否能觸發返回操作的代碼如下:

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    //正在轉場
    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
        return NO;
    }
    
    //在導航控制器的根控制器界面
    if (self.navigationController.viewControllers.count <= 1) {
        return NO;
    }
    
    UIViewController *popedController = [self.navigationController.viewControllers lastObject];
    
    if (popedController.base_popGestureDisabled) {
        return NO;
    }
    
    //滿足有效手勢範圍
    CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
    CGFloat popGestureEffectiveDistanceFromLeftEdge = popedController.base_popGestureEffectiveDistanceFromLeftEdge;
    
    if (popGestureEffectiveDistanceFromLeftEdge > 0
        && beginningLocation.x > popGestureEffectiveDistanceFromLeftEdge) {
        return NO;
    }
    
    //右滑手勢
    UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;
    CGPoint transition = [panGesture translationInView:panGesture.view];
    
    if (transition.x <= 0) {
        return NO;
    }
    
    return YES;
}

 

其中navigationController還使用了私有變數“_isTransitioning”,用於判斷交互是否正在進行中。

 

為了使過場動畫過程中,導航欄的交互動畫自然,需要在UIViewController的viewWillAppear方法中,通過swizzle方法調用導航欄顯示或隱藏的動畫方法,所以需要增加一個延遲執行的代碼塊:

- (void)base_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
{
    //如果navigationController不顯示導航欄,直接return
    if (self.navigationBarHidden) {
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    ViewControllerViewWillAppearDelayBlock block = ^(UIViewController *viewController, BOOL animated) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            [strongSelf setNavigationBarHidden:viewController.base_currentNavigationBarHidden animated:animated];
        }
    };
    
    appearingViewController.viewWillAppearDelayBlock = block;
    UIViewController *disappearingViewController = self.viewControllers.lastObject;
    if (disappearingViewController && !disappearingViewController.viewWillAppearDelayBlock) {
        disappearingViewController.viewWillAppearDelayBlock = block;
    }
}

 

實現完整的分類的過程中,使用了一些運行時類型和方法。

1.引用頭文件#import <objc/runtime.h>

2.為分類增加屬性,涉及到瞭如下方法:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

id objc_getAssociatedObject(id object, const void *key)

例如UIViewController (PopGesture)中增加的屬性base_currentNavigationBarHidden的get/set方法:

- (BOOL)base_currentNavigationBarHidden
{
    NSNumber *number = objc_getAssociatedObject(self, _cmd);
    if (number) {
        return number.boolValue;
    }
    
    self.base_currentNavigationBarHidden = NO;
    return NO;
}

- (void)setBase_currentNavigationBarHidden:(BOOL)hidden
{
    self.canUseViewWillAppearDelayBlock = YES;
    
    objc_setAssociatedObject(self, @selector(base_currentNavigationBarHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

 

第一個參數一般為self。

第二個參數const void *key,要求傳入一個地址。

可以聲明一個static char *key;或者static NSString *key;,賦值與否並不重要,因為需要的只是地址,參數為&key。

而上述代碼中使用了_cmd和@selector,作用是一樣的。_cmd返回的是當前方法的SEL,@selector也是返回目標方法的SEL,即是函數地址。

第三個參數即是關聯的值。

第四個參數policy,為枚舉類型,基本對應屬性引用相關的關鍵字:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {

    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */

    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 

                                            *   The association is not made atomically. */

    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 

                                            *   The association is not made atomically. */

    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.

                                            *   The association is made atomically. */

    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.

                                            *   The association is made atomically. */

};

 

3.理解其他的一些運行時類型或方法

typedef struct objc_method *Method;//An opaque type that represents a method in a class definition.

Method class_getInstanceMethod(Class cls, SEL name) //返回實例方法

Method class_getClassMethod(Class cls, SEL name) //返回類方法

IMP method_getImplementation(Method m) //Returns the implementation of a method.

const char *method_getTypeEncoding(Method m) //Returns a string describing a method's parameter and return types.

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) //Adds a new method to a class with a given name and implementation.

IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) //Replaces the implementation of a method for a given class.

void method_exchangeImplementations(Method m1, Method m2) //Exchanges the implementations of two methods.

 

以上方法,可以達到swizzle方法的目的,將分類中新增方法與已有舊的方法交換函數地址,可以作為完全替換(因為運行時,執行的方法名稱仍然為viewWillAppear:,但是指向新增方法地址),也可以在新增方法代碼中調用當前的方法名稱(交換後,當前的方法名稱指向舊方法地址)。例如UIViewController中的下列代碼:

+(void)load
{
    __weak typeof(self) weakSelf = self;
    
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        [weakSelf swizzleOriginalSelector:@selector(viewWillAppear:) withNewSelector:@selector(base_viewWillAppear:)];
        
        [weakSelf swizzleOriginalSelector:@selector(viewDidDisappear:) withNewSelector:@selector(base_viewDidDisappear:)];
    });
}

+(void)swizzleOriginalSelector:(SEL)originalSelector withNewSelector:(SEL)newSelector
{
    Class selfClass = [self class];
    
    Method originalMethod = class_getInstanceMethod(selfClass, originalSelector);
    Method newMethod = class_getInstanceMethod(selfClass, newSelector);
    
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP newIMP = method_getImplementation(newMethod);
    
    //先用新的IMP加到原始SEL中
    BOOL addSuccess = class_addMethod(selfClass, originalSelector, newIMP, method_getTypeEncoding(newMethod));
    if (addSuccess) {
        class_replaceMethod(selfClass, newSelector, originalIMP, method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, newMethod);
    }
}

-(void)base_viewWillAppear:(BOOL)animated
{
    [self base_viewWillAppear:animated];
    
    if (self.canUseViewWillAppearDelayBlock
        && self.viewWillAppearDelayBlock) {
        self.viewWillAppearDelayBlock(self, animated);
    }
    
    if (self.transitionCoordinator) {
        [self.transitionCoordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
            if ([context isCancelled]) {
                self.base_isBeingPoped = NO;
            }
        }];
    }
}

 

特別說明:

A.+load靜態方法將在此分類加入運行時調用(Invoked whenever a class or category is added to the Objective-C runtime),執行順序在該類自己的+load方法之後。

 

B.如果在使用中未明確設置base_currentNavigationBarHidden,canUseViewWillAppearDelayBlock則為NO,因為我封裝的父類中提供了類似功能,所以不需要開啟分類中同樣的功能。該功能目的是提供給直接集成分類的朋友。

C.UIViewController的transitionCoordinator屬性,在當前界面有過場交互時候,該屬性有值。並且在交互結束時候,可以回調一個block,以告知過場交互的狀態和相關屬性。這裡聲明瞭一個屬性base_isBeingPoped,用於標記當前視圖控制器是否正在被pop出導航棧,如果交互取消了,置為NO,最終可以在viewDidDisappear:方法中判斷並執行一些操作。

 

使用UINavigationController的代理方法來實現高度自定義的方案,下次再更新記錄。

=================

已更新:http://www.cnblogs.com/ALongWay/p/5896982.html

 

base項目已更新:[email protected]:ALongWay/base.git


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 說明 跨域主要是由於瀏覽器的“同源策略”引起,分為多種類型,本文主要探討Ajax請求跨域問題 前言 參考來源 什麼是跨域 ajax跨域的表現 跨域的原理 如何解決跨域問題 JSONP方式解決跨域問題 CROS解決跨域問題 CROS請求原理 PHP後臺配置 JAVA後臺配置 .NET後臺配置 FAQ ...
  • 開本系列,討論一些有趣的 CSS 題目,拋開實用性而言,一些題目為了拓寬一下解決問題的思路,此外,涉及一些容易忽視的 CSS 細節。 解題不考慮相容性,題目天馬行空,想到什麼說什麼,如果解題中有你感覺到生僻的 CSS 屬性,趕緊去補習一下吧。 不斷更新,不斷更新,不斷更新,重要的事情說三遍。 彙總在 ...
  • × 目錄 [1]作用域安全 [2]惰性載入 [3]函數綁定 前面的話 函數對任何一門語言來說都是一個核心的概念,在javascript中更是如此。前面曾以深入理解函數系列的形式介紹了函數的相關內容,本文將再深入一步,介紹函數的3個高級技巧 技巧一:作用域安全的構造函數 構造函數其實就是一個使用new ...
  • 首先看演示: FrameLayout框架佈局是最簡單的佈局形式。所有添加到這個佈局中的視圖都以層疊的方式顯示。第一個添加的控制項被放在最底層,最後一個添加到框架佈局中的視圖顯示在最頂層,上一層的控制項會覆蓋下一層的控制項。這種顯示方式有些類似於堆棧。 當我們往裡面添加組件的時候,所有的組件都會放置於這塊區 ...
  • 這個類提供了對於音頻文件的描述An audio stream is a continuous series of data that represents a sound, such as a song.A channel is a discrete track of monophonic audi... ...
  • 幀動畫——FrameAnimation 將一系列圖片有序播放,形成動畫的效果。其本質是一個Drawable,是一系列圖片的集合,本身可以當做一個圖片一樣使用 在Drawable文件夾下,創建animation-list為根節點的資源文件 <animation-list android:oneshot ...
  • PhotoKit 是ios9以後取代 ALAssetLibrary 的一種新框架,註意是框架不是頭文件,不是可掉出來的系統名只是一個框架名,代表一個框架種類。所以你在xcode中你輸photoKit什麼也沒有,當你輸了n遍試了n遍之後你會發現什麼有沒有。納里什麼鬼,呵呵你被坑了吧。那麼大家就要問了怎 ...
  • 在程式開發中,數據層永遠是程式的核心結構之一。我們將現實事物進行抽象,使之變成一個個數據。對這些數據的加工處理是代碼中能體現技術水平的一大模塊,比如數據的請求、解析、緩存、持久化等等。適當的對數據進行持久化存儲可以實現應用的離線功能,以此提高用戶體驗。在iOS開發中,蘋果提供了四種持久化方案供我們選 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...