1.為啥要重復(fù)造輪子
想要做這個(gè)側(cè)滑功能是因?yàn)槲覀冺?xiàng)目中有使用到側(cè)滑的菜單厕诡,開始我們也沒有使用另外一些比較出名的側(cè)滑框架党远,因?yàn)樵赨I這部分個(gè)人不是很喜歡用第三方摘能,總感覺有時(shí)候不太符合界面的自定義焚辅,而且每開發(fā)一個(gè)自己沒做過的功能自己實(shí)現(xiàn)一次也是對(duì)自己的一種鍛煉,我們當(dāng)時(shí)app的側(cè)滑,就是直接在window的左側(cè)擺了一個(gè)控制器的view拨匆,根據(jù)事件對(duì)這個(gè)view和根控制器進(jìn)行移動(dòng)交互饶氏,之后也有看一些比較出名的側(cè)滑框架,發(fā)現(xiàn)其實(shí)現(xiàn)原理都是類似的,耦合度非常高盏缤,框架替換成本也高,且每次打開UI層次解析界面的時(shí)候蓖扑,整個(gè)window上面總是自帶著這一坨隱藏在背后控制器的view唉铜,看上去有點(diǎn)兒不爽。例如下面這個(gè)圖律杠,剛啟動(dòng)程序就是這樣的:
總感覺不是那么好潭流,于是在結(jié)合之前有看到過的自定義的轉(zhuǎn)場(chǎng)動(dòng)畫(UIViewControllerAnimatedTransitioning)腦袋里冒出一個(gè)想法,是否可以使用系統(tǒng)的push或者present通過自定義轉(zhuǎn)場(chǎng)時(shí)候的動(dòng)畫來實(shí)現(xiàn)它呢柜去,自己覺得可行灰嫉,于是擼起袖子開始干!關(guān)于控制器的轉(zhuǎn)場(chǎng)動(dòng)畫的基本學(xué)習(xí)诡蜓,可以看看這篇文章 iOS7中的ViewController切換
2.未知標(biāo)題
我們的優(yōu)勢(shì):
- 整個(gè)框架沒有任何限制與依賴熬甫,全程類似系統(tǒng)Push操作,你給我一個(gè)控制器蔓罚,我還你一個(gè)側(cè)滑抽屜效果,甚至你可以設(shè)置10個(gè)不同的側(cè)滑抽屜瞻颂。
- 對(duì)原有框架0污染0侵入豺谈,不需要設(shè)置什么LeftVC,rightVC贡这,middleVC這些東西茬末,也不需要繼承自啥TabarController,直接使用0耦合盖矫!
- 當(dāng)抽屜界面在關(guān)閉的情況下丽惭,抽屜界面安全釋放,不會(huì)一直存在內(nèi)存中辈双,界面也不會(huì)一直藏在控制器下或者屏幕外责掏。
先看一下目前我們可以實(shí)現(xiàn)的效果
scrollView嵌套的場(chǎng)景
我們?cè)谥貜?fù)顯示左邊以及右邊菜單之后UI的層級(jí)
正如,代碼虐我千百遍湃望,我待代碼如初戀换衬,不管你怎么弄,怎么操作证芭,最后我還是原來純潔的樣子~
3.如何使用瞳浦?
如果你想實(shí)現(xiàn)目前QQ這種側(cè)滑(上圖左按鈕事件),我們的使用非常簡(jiǎn)單7鲜俊叫潦!真正的一行代碼,一毛一樣騙人是小狗??~首先導(dǎo)入 #import "UIViewController+CWLateralSlide.h" 然后在需要顯示左側(cè)的控制器的時(shí)候調(diào)用cw_showDrawerViewController:方法:
// 導(dǎo)航欄左邊按鈕的點(diǎn)擊事件
- (void)leftClick {
// 自己隨心所欲創(chuàng)建的一個(gè)控制器
LeftViewController *vc = [[LeftViewController alloc] init];
// 調(diào)用這個(gè)方法
[self cw_showDrawerViewController:vc animationType:CWDrawerAnimationTypeDefault configuration:nil];
}
耦合度非常低官硝,想側(cè)滑出哪個(gè)控制器直接傳值需要滑出來的VC就OK矗蕊,不需要提前配置任何元素四敞。任何地方都能調(diào)用,是不是so easy~
4.這個(gè)框架的實(shí)現(xiàn)拔妥。
實(shí)際上就是使用了系統(tǒng)的present方法忿危,我們做的僅僅只是把present這個(gè)動(dòng)畫自定義了,簡(jiǎn)單說一下在寫這個(gè)框架過程中需要注意幾個(gè)坑没龙,但是在這之前铺厨,強(qiáng)烈建議先看看如何自定義轉(zhuǎn)場(chǎng)動(dòng)畫,不然會(huì)一臉懵逼硬纤。
首先如何自定義轉(zhuǎn)場(chǎng)動(dòng)畫我們就不多說了解滓,網(wǎng)上有非常多優(yōu)秀的文章都有說到,可以自行搜索一下筝家,比如這個(gè)iOS自定義轉(zhuǎn)場(chǎng)詳解03總共有4個(gè)demo洼裤,而且這里面有非常多經(jīng)典的動(dòng)畫效果。有興趣可以學(xué)習(xí)一波溪王。腮鞍。說幾個(gè)在寫功能時(shí)候的坑
a、按照流程莹菱,寫好動(dòng)畫~但是在轉(zhuǎn)場(chǎng)動(dòng)畫完成的時(shí)候移国,根控制器消失了!5牢啊<W骸!
我們的根控制并沒有使用截屏圖放在后面用來做動(dòng)畫蜜徽,而是直接將控制器的view放在動(dòng)畫容器containerView內(nèi)(為啥不用更方便的截圖來做動(dòng)畫祝懂,是因?yàn)樽屑?xì)看過QQ的側(cè)滑,發(fā)現(xiàn)在打開菜單的狀態(tài)下,QQ右側(cè)界面在接收到消息還是會(huì)跟著變拘鞋,所以QQ不是用的截屏圖來做動(dòng)畫的砚蓬,于是,它能那樣實(shí)現(xiàn)掐禁,咱們肯定也是能實(shí)現(xiàn)的對(duì)吧)怜械,在實(shí)現(xiàn)的過程中發(fā)現(xiàn)這樣會(huì)導(dǎo)致在動(dòng)畫結(jié)束的時(shí)候,根控制器這邊的view是消失傅事,如下圖:
當(dāng)時(shí)有點(diǎn)沒想明白缕允,看了一些網(wǎng)上大神的資料,有一些用截屏的圖片做動(dòng)畫的不會(huì)有這個(gè)問題(但我們已經(jīng)明確QQ不是用的截屏圖蹭越,所以咱們肯定也可以用這個(gè)之外的方式解決是吧)障本,還有一些是設(shè)置控制器的modalPresentationStyle為UIModalPresentationCustom,設(shè)置之后發(fā)現(xiàn)顯示側(cè)滑的時(shí)候正常了,哇驾霜,可行案训,以為OK了,但是當(dāng)返回之后界面又消失了=粪糙。=强霎!就像這樣~
所以這樣也不是特別好,最后我們從源頭想了想蓉冈,為什么我動(dòng)畫結(jié)束之后view會(huì)消失城舞,無非就是幾種情況,1寞酿、frame不對(duì)家夺,2、透明度為0或者設(shè)置為hidden了伐弹,3拉馋、就是被從父視圖移除了,顯然1惨好,2理論上都不會(huì)發(fā)生煌茴,最終我們?cè)趧?dòng)畫結(jié)束的時(shí)候打印根控制器view的父視圖發(fā)現(xiàn)為nil,找到原因我們就解決它昧狮,所以我們?cè)趧?dòng)畫結(jié)束之后景馁,又把該view添加到containerView上:
if (![transitionContext transitionWasCancelled]) {
[transitionContext completeTransition:YES];
// 動(dòng)畫完成,再次添加根視圖的view
[containerView addSubview:fromVC.view];
maskView.userInteractionEnabled = YES;
}else {
[transitionContext completeTransition:NO];
[imageV removeFromSuperview];
}
這樣就完全沒問題了~具體為啥動(dòng)畫結(jié)束要被移除我也不是很明白逗鸣,但是我猜可能是出于內(nèi)存管理考慮,添加上去之后引用計(jì)數(shù)又+1不利于界面釋放绰精,所以動(dòng)畫結(jié)束又移除掉了撒璧,引用計(jì)數(shù)-1,讓這個(gè)動(dòng)畫過程不對(duì)view的內(nèi)存產(chǎn)生影響笨使,知道為啥的童鞋可以留言指導(dǎo)一下卿樱。在蘋果官方文檔上completeTransition: 這個(gè)方法有說明:The default implementation of this method calls the animator object’s animationEnded(:) method to give it a chance to perform any last minute cleanup.(調(diào)用completeTransition: 之后 會(huì)默認(rèn)調(diào)用animationEnded(:) 方法來進(jìn)行清理工作,所以我們的猜測(cè)應(yīng)該是正確的硫椰,動(dòng)畫完成就被清理了繁调。。)
b靶草、還有一個(gè)細(xì)節(jié)就是在做手勢(shì)驅(qū)動(dòng)結(jié)束的時(shí)候動(dòng)畫不連貫蹄胰。
這個(gè)我們要先說一下在手勢(shì)過程中,重要的幾個(gè)方法
// 手勢(shì)過程中奕翔,更新轉(zhuǎn)場(chǎng)執(zhí)行的進(jìn)度裕寨,參數(shù)為所傳的百分比
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
// 調(diào)用此方法會(huì)取消轉(zhuǎn)場(chǎng),并且回到轉(zhuǎn)場(chǎng)動(dòng)畫之前的狀態(tài)
- (void)cancelInteractiveTransition;
// 手勢(shì)結(jié)束之后,如果想完成轉(zhuǎn)場(chǎng)調(diào)用此方法
- (void)finishInteractiveTransition;
就是我們?cè)谑謩?shì)過程中宾袜,會(huì)傳一個(gè)百分比去調(diào)用第一個(gè)方法捻艳,然后一般會(huì)設(shè)置一個(gè)轉(zhuǎn)場(chǎng)是取消還是完成的臨界點(diǎn),如果在手勢(shì)過程中庆猫,超過這個(gè)臨界點(diǎn)认轨,我們就調(diào)用完成轉(zhuǎn)場(chǎng),如果不到這個(gè)臨界點(diǎn)月培,我們就取消嘁字,比如我們臨界點(diǎn)設(shè)置為轉(zhuǎn)場(chǎng)完成的50%也就是0.5,那么在手勢(shì)過程中超過0.5的時(shí)候我們會(huì)調(diào)用finishInteractiveTransition方法完成轉(zhuǎn)場(chǎng)节视,但是轉(zhuǎn)場(chǎng)動(dòng)畫的更新會(huì)從0.5突然跳到1.0拳锚,這時(shí)屏幕就會(huì)閃一下,甚至不僅僅是閃一下寻行,還會(huì)彈幾下再結(jié)束動(dòng)畫霍掺,就像這樣:
在手勢(shì)過程不到臨界點(diǎn)值時(shí)調(diào)用取消動(dòng)畫的時(shí)候照樣會(huì)出現(xiàn)這個(gè)問題。
提供一下我們的解決思路:
啟動(dòng)一個(gè)定時(shí)器拌蜘,將未完成的動(dòng)畫用一個(gè)定時(shí)器一步步更新到動(dòng)畫結(jié)束后再調(diào)用完成轉(zhuǎn)場(chǎng)或者取消轉(zhuǎn)場(chǎng)杆烁,也就是說,比如我們現(xiàn)在手勢(shì)拖動(dòng)動(dòng)畫更新到0.5简卧,這個(gè)時(shí)候我們松手兔魂,如果直接調(diào)用完成轉(zhuǎn)場(chǎng),界面就會(huì)從0.5猛的跳到1.0举娩,中間的這段動(dòng)畫就會(huì)閃過去或者異常析校,于是我們將0.5-1.0這段時(shí)間的更新用一個(gè)定時(shí)器從0.5跑到1.0再調(diào)用完成手勢(shì)切換,那么整個(gè)轉(zhuǎn)場(chǎng)動(dòng)畫的過程就會(huì)變的非常流暢~我們定時(shí)器用的是CADisplayLink铜涉,實(shí)現(xiàn)方式如下:
- (void)startDisplayLink:(CGFloat)percent{
// 首先判斷是完成轉(zhuǎn)場(chǎng)動(dòng)畫還是取消轉(zhuǎn)場(chǎng)動(dòng)畫
_toFinish = percent > 0.5;
// 再根據(jù)動(dòng)畫總時(shí)長(zhǎng)求得剩下的歸0(取消)或者完成轉(zhuǎn)場(chǎng)動(dòng)畫需要執(zhí)行的時(shí)長(zhǎng)
CGFloat remainDuration = _toFinish ? self.duration * (1 - percent) : self.duration * percent;
// 以每秒60次刷新計(jì)算定時(shí)器需要執(zhí)行多少次
_remaincount = 60 * remainDuration;
// 算出定時(shí)器每執(zhí)行一次需要改變的動(dòng)畫百分比
_oncePercent = _toFinish ? (1 - percent) / _remaincount : percent / _remaincount;
// 開始定時(shí)器智玻,每執(zhí)行一次定時(shí)器,定時(shí)器剩余次數(shù)-1芙代,當(dāng)剩余0次時(shí)吊奢,結(jié)束定時(shí)器,完成轉(zhuǎn)場(chǎng)纹烹。
[self starDisplayLink];
}
處理完這些页滚,我們就獲得了一個(gè)非常流暢的側(cè)滑動(dòng)畫,很開心~
c铺呵、但是裹驰。。開心不過3秒陪蜻。發(fā)現(xiàn)由于我們的側(cè)滑出來的控制器實(shí)際上是present出來的邦马,沒有導(dǎo)航控制器!!W探邻悬!那我們要在這個(gè)控制器里面進(jìn)行push操作怎么辦?随闽?一臉懵逼~
還好父丰,咱們有QQ可以借鑒一下,因?yàn)槲覀兇竽懙牟聹y(cè)QQ就是present的掘宪,所以為啥QQ側(cè)滑的控制器可以push呢蛾扇,在使用中發(fā)現(xiàn),QQ在push的時(shí)候應(yīng)該是先把側(cè)邊控制器dismiss魏滚,然后再使用根控制器tabbarController的第一個(gè)控制器的導(dǎo)航控制器進(jìn)行push的镀首,也就是說分為兩步,第一步把左側(cè)present的控制器dismiss鼠次,然后拿到QQ的消息控制器的導(dǎo)航控制器進(jìn)行push操作更哄,為啥會(huì)這么覺得呢,因?yàn)槭褂肣Q push出來的控制器再返回pop的時(shí)候是直接回到根控制器的腥寇!于是我們的push操作這樣去實(shí)現(xiàn)它:
- (void)cw_pushViewController:(UIViewController *)viewController{
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UINavigationController *nav;
if ([rootVC isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabbar = (UITabBarController *)rootVC;
NSInteger index = tabbar.selectedIndex;
nav = tabbar.childViewControllers[index];
}else if ([rootVC isKindOfClass:[UINavigationController class]]) {
nav = (UINavigationController *)rootVC;
}else if ([rootVC isKindOfClass:[UIViewController class]]) {
NSLog(@"This no UINavigationController...");
return;
}
[self dismissViewControllerAnimated:YES completion:nil];
[nav pushViewController:viewController animated:NO];
}
自己定義一個(gè)方法成翩,最終目的就是找到一個(gè)最優(yōu)的導(dǎo)航控制器來進(jìn)行push操作,所以在使用這個(gè)框架的時(shí)候赦役,側(cè)滑出來的控制器要進(jìn)行push操作麻敌,不能使用系統(tǒng)的push方法(畢竟它沒有導(dǎo)航控制器,就算你打死它它也還是不能push呀)掂摔,必須使用我寫的這個(gè)方法术羔,實(shí)現(xiàn)之后push效果如下:
這些細(xì)節(jié)都處理之后,整個(gè)效果基本都沒問題了~所以我們可以開心的去使用這個(gè)框架啦~
5.最后
第一次在簡(jiǎn)書上寫文章乙漓,覺得對(duì)你有所幫助的童鞋幫忙點(diǎn)個(gè)喜歡(多謝??)聂示,想使用這個(gè)框架或者想看一下實(shí)現(xiàn)原理的童鞋前往我的github地址(很簡(jiǎn)單的啦):
走過路過star不要錯(cuò)過~感謝。
目前已經(jīng)支持cocoapods安裝簇秒。PS:第一次做支持cocoapods,真是一把心酸一把淚秀鞭。趋观。成功率在10%不到,加上網(wǎng)速又慢锋边,真是極大的鍛煉了我的耐心皱坛。能成功搜索到之后真是淚牛滿面。如果遇到什么問題或者優(yōu)化建議歡迎留言豆巨。剩辟。畢竟這是我的第!一!次7妨浴P芑А!要踐踏吭服,請(qǐng)溫柔~~嚷堡。