自定義VCStack

背景介紹

最近開發(fā)的幾個(gè)工程使用的都是系統(tǒng)的VCStack,即UITabbarController + UINavigationController的方式蜈出。這是一個(gè)經(jīng)典的組合详民,在現(xiàn)實(shí)的開發(fā)場(chǎng)景中基本已經(jīng)能夠滿足需求延欠。但是,最近幾期UI稿和UE稿的設(shè)計(jì)規(guī)則沈跨,有點(diǎn)超出了這個(gè)既有框架的能力

  • 遮罩全屏的半浮層
  • 出棧入棧復(fù)雜的動(dòng)畫
  • 跨VC堆棧的poppush操作

在現(xiàn)有的UITabbarController + UINavigationController結(jié)構(gòu)下由捎,這些功能已經(jīng)被實(shí)現(xiàn),但是過程較為復(fù)雜饿凛,不少邏輯現(xiàn)在看來任有優(yōu)化的空間狞玛,基于這個(gè)背景,打算寫一個(gè)自定義的VCStack涧窒,解決系統(tǒng)空間的局限性

系統(tǒng)VCStack存在的困境

在構(gòu)思自定義VCStack之前心肪,回顧了一下系統(tǒng)控件在日常開發(fā)中存在的瓶頸,這些瓶頸在日查那個(gè)的業(yè)務(wù)開發(fā)中經(jīng)常困擾著我們纠吴,拖累開發(fā)人員的效率硬鞍。總結(jié)了一下戴已,有以下幾點(diǎn):

  • UI***Bar層級(jí)過高導(dǎo)致的頁面遮擋問題
  • 出/入棧動(dòng)畫支持不夠友好的問題
    是的固该,我們可以通過NavigationControllerDelegate的方式,在代理中完成自定義動(dòng)畫的實(shí)現(xiàn)糖儡。但是這個(gè)代理的接入往往強(qiáng)依賴在某一個(gè)頁面蹬音,抽象的層次不夠,復(fù)用性也不高休玩。不符合要求
  • 任意時(shí)間點(diǎn)getTopVC帶來的問題
    堆棧的操作往往伴隨著動(dòng)畫,動(dòng)畫中包含時(shí)間劫狠,如果我們?cè)诓缓线m的時(shí)間節(jié)點(diǎn)getTopVC可能導(dǎo)致之后的UI操作完全失效拴疤。比如,view正在消失的時(shí)候獲取topVC并在vc.view中增加UI的處理
  • 布局標(biāo)準(zhǔn)的問題独泞。
    由于TopLayout和BottomLayout的存在呐矾,導(dǎo)致我們的布局原點(diǎn)在一些操作中可能發(fā)生改變,這樣的情況需要一定的開發(fā)經(jīng)驗(yàn)才能捕捉到懦砂。一旦人為遺漏就可能造成布局上的錯(cuò)誤
  • 交叉影響蜒犯。
    這里舉一個(gè)例子:修改Navigation的backItem會(huì)導(dǎo)致系統(tǒng)默認(rèn)的優(yōu)化手勢(shì)失效组橄,需要復(fù)寫此功能才能生效
  • 指定堆棧的跳轉(zhuǎn)。
    系統(tǒng)當(dāng)前沒有提供一個(gè)統(tǒng)一的調(diào)度入口來解決跨VC的跳轉(zhuǎn)的問題罚随,當(dāng)前的實(shí)現(xiàn)還是基于遍歷來找到VC實(shí)現(xiàn)跳轉(zhuǎn)
  • 模態(tài)視圖繼續(xù)跳轉(zhuǎn)的問題
    這是一種經(jīng)常出現(xiàn)的場(chǎng)景,模態(tài)一個(gè)視圖玉工,在這個(gè)模態(tài)視圖的基礎(chǔ)上還存在堆棧的操作。當(dāng)前的實(shí)現(xiàn)大多是在模態(tài)的基礎(chǔ)上再包一層NavigationController,讓其具備堆棧操作的能力

上面幾個(gè)case使我們自定義VCStack解決的核心問題淘菩,本文也會(huì)按照這幾個(gè)痛點(diǎn)展開講解是如何一一解決這些問題的

自定義VCStack是什么

先交代一下這個(gè)VCStack到底是什么遵班,系統(tǒng)NavigationController的效果我們都不陌生,如何在不繼承系統(tǒng)NavigationController的基礎(chǔ)上實(shí)現(xiàn)一套自己的VCStack管理機(jī)制呢(保持效果一致的原則)潮改?從日常的使用中狭郑,我們了解到系統(tǒng)的NavigationController其實(shí)一個(gè)堆棧管理器,之中最重要的是VC的管理汇在,可能是頂層封裝的原因使得我們對(duì)整個(gè)管理體系了解不多翰萨。但是有幾點(diǎn)是可以猜測(cè)到的

1、所有的VC都擁有自己的View
2糕殉、所有的View都是在根Window上展示的
3亩鬼、你看到的動(dòng)畫只是管理器讓交互不再生硬做出的表象

意識(shí)到這三點(diǎn),接下來就好辦了糙麦,VC是獨(dú)立的辛孵,可以在任意節(jié)點(diǎn)創(chuàng)建和銷毀,我們的VCStack只需要管理他們的顯示邏輯和已有的生命周期赡磅。所以VCStack只要找到切合的時(shí)間點(diǎn)疊加和管理這些VC即可魄缚。首先有個(gè)統(tǒng)一的入口

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

這個(gè)節(jié)點(diǎn)中window需要一個(gè)rootViewController,這是VCStack接入的切口,一個(gè)VC創(chuàng)建并作為RootViewController被VCStack持有焚廊,VCStackInstance.rootViewController作為參數(shù)給到Window冶匹。這一步操作已經(jīng)為VCStack打下了基石,因?yàn)橹笏蠽C.view的疊加都有了rootView.接下來的事情就變的簡(jiǎn)單了

1咆瘟、push操作將vc.view疊加到currentVC
2嚼隘、pop操作將vc.view從上一個(gè)vc.view移除

這期間需要兼顧的東西還有很多,比如

1袒餐、vc生命周期的一致
2飞蛹、手勢(shì)操作
3、動(dòng)畫接入

對(duì)整個(gè)想做的事情有了一定的了解了之后灸眼,下面是一些實(shí)現(xiàn)中的細(xì)節(jié)

逐個(gè)擊破

視圖層級(jí) + 布局原點(diǎn)

自定義VCStack不會(huì)再有TopLayout和BottomLayout這種預(yù)置依賴卧檐,所有的View的布局都將從window的(0,0)點(diǎn)開始布局焰宣。navigationBarTabBar也將會(huì)被CustomView代替以此抹平層級(jí)間Z軸差距過大導(dǎo)致的遮罩問題
[圖片上傳中...(系統(tǒng)navigation層級(jí).png-6d0e8b-1545878789170-0)]

自定義層級(jí).png

當(dāng)Window的整個(gè)區(qū)域都有權(quán)限去管理之后霉囚,層級(jí)和布局原點(diǎn)的問題就已經(jīng)不是問題了,但是這樣又引入了其他問題:

  1. 自定義navigationBar增加了每個(gè)頁面開發(fā)的成本
  2. 自定義TabBar增加了每個(gè)頁面開發(fā)的成本

一個(gè)好的方法就是創(chuàng)建一個(gè)快捷的模板類匕积,將常用的NavigationBar和常用的TabBar封裝成模板輸出盈罐,增加開發(fā)效率

@interface UIViewController (NavigationBar)
- (HDDefaultNaviBar *)defaultBar;
@end

- (HDDefaultNaviBar *)defaultBar {
    HDDefaultNaviBar *customerBar = [[HDDefaultNaviBar alloc] initWithFrame:CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.navigationBarHeight + HDScreenInfo.statusBarHeight)];
    customerBar.backgroundColor = [UIColor whiteColor];
    customerBar.title = @"測(cè)試title";
    customerBar.backIcon = [UIImage imageNamed:@"NaviBack"];
    customerBar.backAction = ^{
        [self.vcStack popWithAnimation:[HDVCStackAnimation defaultAnimation]];
    };
    return customerBar;
}

動(dòng)畫拓展性

系統(tǒng)的Navigation堆棧的跳轉(zhuǎn)提供的api并不多

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; // Uses a horizontal slide transition. Has no effect if the view controller is already in the stack
- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated; // Returns the popped controller.

跳轉(zhuǎn)中動(dòng)畫的支持方式為Bool值揍很,這就限定了跳轉(zhuǎn)中的動(dòng)畫拓展性栗精。當(dāng)然,設(shè)計(jì)系統(tǒng)的人為了能讓跳轉(zhuǎn)中的動(dòng)畫得到更高粒度的支持,實(shí)現(xiàn)了NavigationControllerDelegate這套協(xié)議膘融,在集成了這套協(xié)議的VC中舷夺,可以將動(dòng)畫拓展的更好茵瘾,協(xié)議如下:

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);

但是任然有缺陷踢关,細(xì)想一下,這樣的協(xié)議是在哪個(gè)層面實(shí)現(xiàn)呢库物?

1霸旗、直接耦合到需要?jiǎng)赢嬛С值腣C?
2、抽象到UIViewController層面的統(tǒng)一代理戚揭?

1的方式在實(shí)際的使用中诱告,算是較多的一種,但是存在拓展性和邏輯抽象的問題民晒,相同的問題在另一個(gè)場(chǎng)景下精居,大多的復(fù)用方式是:copy + 粘貼。場(chǎng)景少還能理解潜必,一旦這樣場(chǎng)景多了靴姿,這種方式帶來的問題就會(huì)凸顯出來。漸漸的在使用系統(tǒng)VCStack的基調(diào)下磁滚,就會(huì)有人抽象這個(gè)層面的信息佛吓,做一個(gè)統(tǒng)一的管理,形成了2的這種方式垂攘,但是维雇,2這種方式也是存在問題的,先看一下抽象層面的信息:

  • currentVC
  • willShowVC
  • operation

關(guān)鍵點(diǎn)出在了operation晒他,這是系統(tǒng)的枚舉類型吱型,和業(yè)務(wù)場(chǎng)景中的契合度不是很高,限制了動(dòng)畫的類型陨仅。這相當(dāng)于找到了這個(gè)動(dòng)畫支持的痛點(diǎn)津滞,現(xiàn)在講一下我的思路:

在自定義的VCStack中將動(dòng)畫完全交出去,以實(shí)例的形式交出去灼伤,這看起來有點(diǎn)難以理解据沈。如何統(tǒng)一實(shí)例的api?這就用到了協(xié)議饺蔑。所有的animation實(shí)例是繼承AnimationProtocol的,由這個(gè)協(xié)議來約束api,使得所有實(shí)例的調(diào)度一致嗜诀。結(jié)構(gòu)如下:


Animation設(shè)計(jì).png

下面是實(shí)例的生成api,在實(shí)際的使用中每個(gè)獨(dú)具特色的動(dòng)畫協(xié)議都是這么寫的猾警,他們的具體實(shí)現(xiàn)放在了集成的協(xié)議中

@interface HDVCStackAnimation : NSObject <HDVCStackAnimationProtocol>
+ (instancetype)defaultAnimation;
@end

協(xié)議本身和堆棧的邏輯保持一致

@protocol HDVCStackAnimationProtocol <NSObject>
- (void)pushWithWillShowVC:(UIViewController *)willShowVC
                 currentVC:(UIViewController *)currentVC
                completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);

- (void)popWithWillShowVC:(UIViewController *)willShowVC
                currentVC:(UIViewController *)currentVC
               completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);

@end

協(xié)議的實(shí)現(xiàn)也是面向切面的孔祸,只需要關(guān)注當(dāng)前的參數(shù)和邏輯,例如如下是一個(gè)模擬系統(tǒng)自帶的堆棧動(dòng)畫的協(xié)議實(shí)現(xiàn)

@implementation HDVCStackAnimation
+ (instancetype)defaultAnimation {
    return [HDVCStackAnimation new];
}

- (void)pushWithWillShowVC:(UIViewController *)willShowVC
                 currentVC:(UIViewController *)currentVC
                completion:(void (^)(BOOL))completion {
    // 動(dòng)畫開始前的UI效果
    willShowVC.view.frame = CGRectMake(HDScreenInfo.width, 0, HDScreenInfo.width, HDScreenInfo.height);
    [UIView animateWithDuration:0.34 animations:^{
        willShowVC.view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
        currentVC.view.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
    } completion:^(BOOL finished) {
        if (finished) {
            /* 將對(duì)應(yīng)View的frame還原
             保持和無動(dòng)畫的邏輯對(duì)應(yīng)
             同時(shí)保證在UI調(diào)試時(shí)的正確性
             */
            willShowVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
            currentVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
        }
        completion(finished);
    }];
}

- (void)popWithWillShowVC:(UIViewController *)willShowVC
                currentVC:(UIViewController *)currentVC
               completion:(void (^)(BOOL))completion {
    // 動(dòng)畫開始前的UI效果
    willShowVC.view.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
    currentVC.view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
    [UIView animateWithDuration:0.34 animations:^{
        willShowVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
        currentVC.view.frame = CGRectMake(HDScreenInfo.width, 0, HDScreenInfo.width, HDScreenInfo.height);
    } completion:^(BOOL finished) {
        completion(finished);
    }];
}
@end

調(diào)用API的簡(jiǎn)化:

[self.vcStack pushto:vc animation:[HDVCStackAnimation defaultAnimation]];

可以看到发皿,優(yōu)化之后的動(dòng)畫api參數(shù)也是三個(gè)

  • currentVC
  • willShowVC
  • AnimationInstance

但是這里的animationInstance實(shí)現(xiàn)的空間大大增加崔慧,他只要繼承自AnimationProtocol,具體的animation如何實(shí)現(xiàn)已經(jīng)完全交給了業(yè)務(wù)層穴墅。如果在業(yè)務(wù)層的設(shè)計(jì)上適配幾套符合當(dāng)前場(chǎng)景的animation惶室,這樣的抽象也會(huì)被簡(jiǎn)化到為數(shù)不多的Animation實(shí)例中。滿足了我們的要求玄货,拓展性和邏輯抽象

getTopVC + 交叉影響

在完全接手了VCStack之后皇钞,對(duì)于操作的每個(gè)細(xì)節(jié)都在開發(fā)者的掌握之中,當(dāng)任務(wù)觸達(dá)的時(shí)候松捉,可以追加AnimationCompletionHandle的處理夹界,來讓這個(gè)邏輯更加健壯。同樣的交叉影響的存在也被開發(fā)人員決定隘世,只有設(shè)計(jì)中存在這種交叉影響可柿,才會(huì)在使用中存在這樣的邏輯。設(shè)計(jì)的節(jié)點(diǎn)已經(jīng)被開發(fā)人員管控丙者,需不需要這種邏輯交互已經(jīng)不再是一個(gè)黑盒

getTopVC+交叉影響.png
指定VC的跳轉(zhuǎn)

這個(gè)功能在實(shí)際的業(yè)務(wù)中會(huì)經(jīng)常遇到复斥,在系統(tǒng)Navigation的基礎(chǔ)上的實(shí)現(xiàn)如下

1、遍歷navigationController.viewControllers
2械媒、找到匹配的VC實(shí)例
3目锭、執(zhí)行popToVC操作

前面兩步基本不可避免,導(dǎo)致在實(shí)際的落地式往往一堆一堆代碼的存在滥沫,對(duì)于代碼簡(jiǎn)潔來說不是一個(gè)很好的方案侣集。考慮到這樣的需求場(chǎng)景兰绣,VCStack中集成了一套快捷的跳轉(zhuǎn)API,覆蓋了常見的業(yè)務(wù)場(chǎng)景

/**
 push 操作世分,向當(dāng)前堆棧中r壓入一個(gè)對(duì)象

 @param vc 即將被入棧的viewController
 @param animation 入棧動(dòng)畫
 */
- (void)pushto:(UIViewController *)vc
     animation:(NSObject<HDVCStackAnimationProtocol> *)animation;

/**
 出棧操作

 @param animation 出棧動(dòng)畫
 */
- (void)popWithAnimation:(NSObject<HDVCStackAnimationProtocol> *)animation;

/**
 出棧到根節(jié)點(diǎn)操作

 @param animation 出棧動(dòng)畫類型
 */
- (void)popToRootViewControllerWithAnimation:(NSObject<HDVCStackAnimationProtocol> *)animation;

/**
 出棧到指定的vc操作,匹配條件是當(dāng)前的vc名稱

 @param vcName 即將要顯示的vc名稱
 @param popAnimation 出棧動(dòng)畫
 */
- (void)popToVCWithName:(NSString *)vcName
    animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation;

/**
 出棧到指定的vc,匹配條件是實(shí)例對(duì)象的id指針是否相等

 @param vc 即將要顯示的vc實(shí)例
 @param popAnimation 出棧動(dòng)畫
 @param popCompletion 操作完成之后的回調(diào)缀辩,主要用于pop then push這種操作
 */
- (void)popTo:(UIViewController *)vc
    animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation
popCompleteHandle:(void (^)(BOOL))popCompletion;

/**
 出棧到指定的vc名稱臭埋,之后再壓棧到一個(gè)的vc

 @param popVCName 即將在棧頂出現(xiàn)的vc名稱
 @param popAnimation 出棧動(dòng)畫
 @param pushVC 即將壓棧的vc實(shí)例
 @param pushAnimation 壓棧動(dòng)畫
 */
- (void)popToVCWithName:(NSString *)popVCName
              animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation
             thenPushTo:(UIViewController *)pushVC
              animation:(NSObject<HDVCStackAnimationProtocol> *)pushAnimation;

/**
 出棧到指定的vc實(shí)例,之后再壓棧到一個(gè)的vc

 @param popVC 即將在棧頂出現(xiàn)的vc名稱
 @param popAnimation 出棧動(dòng)畫
 @param pushVC 即將壓棧的vc實(shí)例
 @param pushAnimation 壓棧動(dòng)畫
 */
- (void)popTo:(UIViewController *)popVC
    animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation
   thenPushTo:(UIViewController *)pushVC
    animation:(NSObject<HDVCStackAnimationProtocol> *)pushAnimation;

@end

邏輯的處理已經(jīng)在VCStack內(nèi)部完成臀玄,只需要簡(jiǎn)單的API調(diào)用就可以完成業(yè)務(wù)需求

模態(tài)視圖后續(xù)堆棧跳轉(zhuǎn)

如果在模態(tài)視圖中還存在堆棧的跳轉(zhuǎn)瓢阴,系統(tǒng)VCStack基礎(chǔ)下的處理基本是在modalVC上包裝一層VCStack,使其具備這樣的能力,但是這里會(huì)存在問題健无,兩個(gè)navigationStack的間接斷開荣恐,如果這里執(zhí)行popToVC會(huì)帶了大量的邏輯判斷。使用了自定義VCStack可以將modal視圖的出現(xiàn)規(guī)劃到push操作中,只是這里的動(dòng)畫實(shí)例發(fā)生了改變

@implementation HDModelAnimation
+ (instancetype)defaultAnimation {
    return [HDModelAnimation new];
}

- (void)pushWithWillShowVC:(UIViewController *)willShowVC
                 currentVC:(UIViewController *)currentVC
                completion:(void (^)(BOOL))completion {
    // 動(dòng)畫開始前的UI效果
    willShowVC.view.frame = CGRectMake(0, HDScreenInfo.height, HDScreenInfo.width, HDScreenInfo.height);
    [UIView animateWithDuration:0.34 animations:^{
        willShowVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
    } completion:^(BOOL finished) {
        completion(finished);
    }];
}

- (void)popWithWillShowVC:(UIViewController *)willShowVC
                currentVC:(UIViewController *)currentVC
               completion:(void (^)(BOOL))completion {
    // 動(dòng)畫開始前的UI效果
    [UIView animateWithDuration:0.34 animations:^{
        currentVC.view.frame = CGRectMake(0, HDScreenInfo.height, HDScreenInfo.width, HDScreenInfo.height);
    } completion:^(BOOL finished) {
        completion(finished);
    }];
}
@end

這樣的操作和模態(tài)視圖出現(xiàn)和消失的視覺效果等效叠穆,同時(shí)保持了VCStack鏈

[self.vcStack pushto:vc animation:[HDModelAnimation defaultAnimation]];
[self.vcStack popWithAnimation:[HDModelAnimation defaultAnimation]];

細(xì)節(jié)

在自定義VCStack中設(shè)計(jì)到很多細(xì)節(jié)操作少漆,這些操作的完善會(huì)讓整個(gè)VCStack更加的健壯

生命周期維護(hù)

在VCStack中除了view的依賴的管理,同步操作還需要將對(duì)應(yīng)的VC的生命周期管理起來硼被,在日常的業(yè)務(wù)場(chǎng)景中這幾個(gè)生命周期使用的頻次是最高的

  • viewWillAppear
  • viewDidAppear
  • viewWillDisappear
  • viewDidDisappear
  • dealloc

為了保持和系統(tǒng)生命周期的一致性示损,在push和pop操作中對(duì)VC的生命周期做了手動(dòng)處理

- (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation {
    // 添加手勢(shì)處理
    [self panGestureWithView:vc];
    
    // 當(dāng)前禁止任何手勢(shì)
    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
    [self.viewControllers addObject:vc];
    [vc viewWillAppear:false];
    [self.visibleViewController viewWillDisappear:false];
    [self.visibleViewController.view addSubview:vc.view];
    vc.vcStack = self;

    // 對(duì)底部的tabBar做層級(jí)操作
    if (vc.hdHideBottomBarWhenPushed) {
        // 這里什么都不做
        [self.tabBarManager.view bringSubviewToFront:vc.view];
    }
    
    if (animation) {
        // 動(dòng)畫開始
        [animation pushWithWillShowVC:vc currentVC:self.visibleViewController completion:^(BOOL finished) {
            if (finished) {
                [self.visibleViewController viewDidDisappear:true];
                [vc viewDidAppear:true];
                self.visibleViewController = vc;
                // 手勢(shì)禁用關(guān)閉
                [[UIApplication sharedApplication] endIgnoringInteractionEvents];
            }
        }];
    }
    else {
        // 手勢(shì)禁用關(guān)閉
        [self.visibleViewController viewDidDisappear:false];
        [vc viewDidAppear:false];
        self.visibleViewController = vc;
        [[UIApplication sharedApplication] endIgnoringInteractionEvents];
    }
}

- (void)popToVC:(UIViewController *)popToVC
      animation:(NSObject<HDVCStackAnimationProtocol> *)animation
  willDismissVC:(UIViewController *)willDismissVC
popCompleteHandle:(void (^)(BOOL))popCompletion {
    if (popToVC) {
        // 基礎(chǔ)引用鏈
        willDismissVC.vcStack = nil;
        // 當(dāng)前禁止任何手勢(shì)
        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
        if (animation) {
            [popToVC viewWillAppear:true];
            [willDismissVC viewWillDisappear:true];
            [animation popWithWillShowVC:popToVC currentVC:willDismissVC
                              completion:^(BOOL finished) {
                                  if (finished) {
                                      [willDismissVC.view removeFromSuperview];
                                      [willDismissVC viewDidDisappear:true];
                                      [popToVC viewDidAppear:true];
                                      self.visibleViewController = popToVC;
                                      // 手勢(shì)禁用關(guān)閉
                                      [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                                      // completion handle
                                      if (popCompletion) {
                                          popCompletion(finished);
                                      }
                                  }
                              }];
        }
        else {
            [popToVC viewWillAppear:false];
            [willDismissVC viewWillDisappear:false];
            [willDismissVC.view removeFromSuperview];
            [willDismissVC viewDidDisappear:false];
            [popToVC viewDidAppear:false];
            self.visibleViewController = popToVC;
            // 手勢(shì)禁用關(guān)閉
            [[UIApplication sharedApplication] endIgnoringInteractionEvents];
            if (popCompletion) {
                popCompletion(YES);
            }
        }
    }
    else {
        if (popCompletion) {
            popCompletion(NO);
        }
    }
}

對(duì)于dealloc 在持有鏈消失的時(shí)候能被系統(tǒng)檢測(cè)到,可以正常的釋放嚷硫,當(dāng)前的持有關(guān)系為:

  • VCStack持有數(shù)組
  • 數(shù)組持有VC
  • vc弱持有VCStack

其中VC弱持有VCStack是為了兼容tabBarController的存在检访,如果工程是一個(gè)單一的VCStack完全可以用單例待提升實(shí)例。在pop的時(shí)候會(huì)主動(dòng)解開所有的依賴

VC.vcStack = nil 
VCStack.array remove VC
手勢(shì)系統(tǒng)維護(hù)

在每次push的時(shí)候仔掸,都會(huì)在View的層級(jí)上增加手勢(shì)系統(tǒng)脆贵,當(dāng)然這里也有協(xié)議的支持,如果VC實(shí)現(xiàn)了協(xié)議

@protocol HDVCEnableDragBackProtocol <NSObject>
- (BOOL)enableDrag;
@end

并標(biāo)記為NO的時(shí)候嘉汰,這個(gè)頁面是不支持手勢(shì)的丹禀。具體實(shí)現(xiàn)如下:

- (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation {
    // 添加手勢(shì)處理
    [self panGestureWithView:vc];
    .......
}

- (void)pangestureWithView:(UIView *)view completeHandle:(void (^)(void))completeHandle {
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    self.successBlock = completeHandle;
    [view addGestureRecognizer:panGesture];
}

- (void)pan:(UIPanGestureRecognizer *)pan {
    // 當(dāng)前正在拖動(dòng)的view
    UIView *view = pan.view;
    // 即將要顯示的View
    if (self.viewControllers.count > 1) {
        UIViewController *bottomViewController = self.viewControllers[self.viewControllers.count - 2];
        UIView *bottomView = bottomViewController.view;
        
        // 一些標(biāo)記值
        static CGPoint startViewCenter;
        static CGPoint startBottomViewCenter;
        static BOOL continueFlag = YES;
        
        if (view && bottomView) {
            // 拖動(dòng)開始的檢測(cè)
            if (pan.state == UIGestureRecognizerStateBegan) {
                // 拖動(dòng)開始時(shí)View的frame需要先發(fā)生變化,保證和系統(tǒng)的UI風(fēng)格統(tǒng)一
                bottomView.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                // 檢測(cè)當(dāng)前的拖動(dòng)的位置是否在合適的點(diǎn)鞋怀,當(dāng)前確立双泪,view的左邊1/3z位置可以作為觸發(fā)的初始點(diǎn)
                CGPoint startPoint = [pan locationInView:view];
                if (startPoint.x > (view.frame.size.width / 3.0)) {
                    continueFlag = NO;
                }
                else {
                    continueFlag = YES;
                    // 將底部的View遮罩,避免手勢(shì)點(diǎn)擊造成其他問題
                    [bottomView addSubview:self.maskView];
                }
                startViewCenter = view.center;
                startBottomViewCenter = bottomView.center;
            }
            else if (pan.state == UIGestureRecognizerStateChanged) {
                if (continueFlag) {
                    // 拿到對(duì)一個(gè)的偏移量
                    CGPoint transition = [pan translationInView:view];
                    view.center = CGPointMake(startViewCenter.x + transition.x / 3.0 * 2.0, startViewCenter.y);
                    bottomView.center = CGPointMake(startBottomViewCenter.x + transition.x / 3.0, startBottomViewCenter.y);
                }
            }
            else if (pan.state == UIGestureRecognizerStateEnded) {
                if (continueFlag) {
                    // 將遮罩view去除
                    if (self.maskView.superview != nil) {
                        [self.maskView removeFromSuperview];
                    }
                    // 開始收尾動(dòng)畫
                    if (view.center.x > (view.frame.size.width / 6.0 * 7.0)) {
                        if (self.successBlock) {
                            self.successBlock();
                        }
                    }
                    else {
                        // 禁止用戶操作
                        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
                        // 還原到初始的位置
                        [UIView animateWithDuration:0.34 animations:^{
                            view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                            bottomView.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                        } completion:^(BOOL finished) {
                            if (finished) {
                                // 解開用戶手勢(shì)操作
                                [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                                // 還原對(duì)象的位置
                                view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
                                bottomView.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
                            }
                        }];
                    }
                }
            }
        }
    }
}
動(dòng)畫期間手勢(shì)隔離

自定義VCStack提供了很多便捷的操作API,這些api中很多是伴有animation 操作的密似,為了避免用戶在animation期間響應(yīng)手勢(shì)導(dǎo)致一些未知的錯(cuò)誤焙矛,在代碼段做了容錯(cuò)

- (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation {
    // 添加手勢(shì)處理
    [self panGestureWithView:vc];
    
    // 當(dāng)前禁止任何手勢(shì)
    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
   ........
    
    if (animation) {
        // 動(dòng)畫開始
        [animation pushWithWillShowVC:vc currentVC:self.visibleViewController completion:^(BOOL finished) {
            if (finished) {
                .......
                // 手勢(shì)禁用關(guān)閉
                [[UIApplication sharedApplication] endIgnoringInteractionEvents];
            }
        }];
    }
    else {
        // 手勢(shì)禁用關(guān)閉
       .....
        [[UIApplication sharedApplication] endIgnoringInteractionEvents];
    }
}

// pop 也是同樣的邏輯

// 在右滑手勢(shì)中增加了底部的bottomVC的遮罩,避免左滑手勢(shì)響應(yīng)其他事件帶來問題
if (pan.state == UIGestureRecognizerStateBegan) {
    .....
    else {
           continueFlag = YES;
           // 將底部的View遮罩残腌,避免手勢(shì)點(diǎn)擊造成其他問題
           [bottomView addSubview:self.maskView];
           }
       .......
 }
......
else if (pan.state == UIGestureRecognizerStateEnded) {
      if (continueFlag) {
          // 將遮罩view去除
          if (self.maskView.superview != nil) {
             [self.maskView removeFromSuperview];
          }
  }

總結(jié)

在實(shí)現(xiàn)的過程中村斟,一開始的實(shí)現(xiàn)是圍繞著一個(gè)NavigationStack的方式去進(jìn)行的,這在實(shí)際的開發(fā)中已經(jīng)滿足了大多需求抛猫,因?yàn)榇蠖嗟腶pp都是一個(gè)Navigation的方式管理的蟆盹,即便底部存在多個(gè)業(yè)務(wù)窗口,但是在下一級(jí)頁面都會(huì)關(guān)閉底部的這個(gè)入口闺金。
為了支持系統(tǒng)tabBar和VCStack混合管理的方式逾滥,在原來的基礎(chǔ)上集成了tabBarManager+VCStack。是的整體的邏輯更靠近系統(tǒng)TabBar+navigation的管理方式败匹。

最后說一句項(xiàng)目還在完善中寨昙,如果有興趣可以一并完善。項(xiàng)目地址如下
VCStack
VCStack+TabBarManager

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掀亩,一起剝皮案震驚了整個(gè)濱河市舔哪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌槽棍,老刑警劉巖捉蚤,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抬驴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡缆巧,警方通過查閱死者的電腦和手機(jī)怎爵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盅蝗,“玉大人,你說我怎么就攤上這事姆蘸《漳” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵逞敷,是天一觀的道長(zhǎng)狂秦。 經(jīng)常有香客問我,道長(zhǎng)推捐,這世上最難降的妖魔是什么裂问? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮牛柒,結(jié)果婚禮上堪簿,老公的妹妹穿的比我還像新娘。我一直安慰自己皮壁,他們只是感情好椭更,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛾魄,像睡著了一般虑瀑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上滴须,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天舌狗,我揣著相機(jī)與錄音,去河邊找鬼扔水。 笑死痛侍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的铭污。 我是一名探鬼主播恋日,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼嘹狞!你這毒婦竟也來了岂膳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤磅网,失蹤者是張志新(化名)和其女友劉穎谈截,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡簸喂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年毙死,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喻鳄。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扼倘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出除呵,到底是詐尸還是另有隱情再菊,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布颜曾,位于F島的核電站纠拔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏泛豪。R本人自食惡果不足惜稠诲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诡曙。 院中可真熱鬧臀叙,春花似錦、人聲如沸岗仑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荠雕。三九已至稳其,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間炸卑,已是汗流浹背既鞠。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盖文,地道東北人嘱蛋。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像五续,于是被迫代替她去往敵國(guó)和親洒敏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容