背景介紹
最近開發(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堆棧的
pop
和push
操作
在現(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)開始布局焰宣。navigationBar
和TabBar
也將會(huì)被CustomView代替以此抹平層級(jí)間Z軸差距過大導(dǎo)致的遮罩問題
[圖片上傳中...(系統(tǒng)navigation層級(jí).png-6d0e8b-1545878789170-0)]
當(dāng)Window的整個(gè)區(qū)域都有權(quán)限去管理之后霉囚,層級(jí)和布局原點(diǎn)的問題就已經(jīng)不是問題了,但是這樣又引入了其他問題:
- 自定義navigationBar增加了每個(gè)頁面開發(fā)的成本
- 自定義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)如下:
下面是實(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è)黑盒
指定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