真!一行代碼集成0耦合QQ側滑功能

1.為啥要重復造輪子

想要做這個側滑功能是因為我們項目中有使用到側滑的菜單,開始我們也沒有使用另外一些比較出名的側滑框架,因為在UI這部分個人不是很喜歡用第三方,總感覺有時候不太符合界面的自定義,而且每開發一個自己沒做過的功能自己實現一次也是對自己的一種鍛煉,我們當時app的側滑,就是直接在window的左側擺了一個控制器的view,根據事件對這個view和根控制器進行移動交互,之后也有看一些比較出名的側滑框架,發現其實現原理都是類似的,耦合度非常高,框架替換成本也高,且每次打開UI層次解析界面的時候,整個window上面總是自帶著這一坨隱藏在背后控制器的view,看上去有點兒不爽。例如下面這個圖,剛啟動程序就是這樣的:

示例圖@2x.png

總感覺不是那么好,于是在結合之前有看到過的自定義的轉場動畫(UIViewControllerAnimatedTransitioning)腦袋里冒出一個想法,是否可以使用系統的push或者present通過自定義轉場時候的動畫來實現它呢,自己覺得可行,于是擼起袖子開始干!關于控制器的轉場動畫的基本學習,可以看看這篇文章 iOS7中的ViewController切換

2.未知標題

我們的優勢:

  • 整個框架沒有任何限制與依賴,全程類似系統Push操作,你給我一個控制器,我還你一個側滑抽屜效果,甚至你可以設置10個不同的側滑抽屜。
  • 對原有框架0污染0侵入,不需要設置什么LeftVC,rightVC,middleVC這些東西,也不需要繼承自啥TabarController,直接使用0耦合!
  • 當抽屜界面在關閉的情況下,抽屜界面安全釋放,不會一直存在內存中,界面也不會一直藏在控制器下或者屏幕外。

先看一下目前我們可以實現的效果

示例圖1.gif

scrollView嵌套的場景


scrollView嵌套的場景.gif

我們在重復顯示左邊以及右邊菜單之后UI的層級

111@2x.png

正如,代碼虐我千百遍,我待代碼如初戀,不管你怎么弄,怎么操作,最后我還是原來純潔的樣子~

3.如何使用?

如果你想實現目前QQ這種側滑(上圖左按鈕事件),我們的使用非常簡單!!真正的一行代碼,一毛一樣騙人是小狗??~首先導入 #import "UIViewController+CWLateralSlide.h" 然后在需要顯示左側的控制器的時候調用cw_showDrawerViewController:方法:

// 導航欄左邊按鈕的點擊事件
- (void)leftClick {
    // 自己隨心所欲創建的一個控制器
    LeftViewController *vc = [[LeftViewController alloc] init];
    // 調用這個方法
    [self cw_showDrawerViewController:vc animationType:CWDrawerAnimationTypeDefault configuration:nil];
}

耦合度非常低,想側滑出哪個控制器直接傳值需要滑出來的VC就OK,不需要提前配置任何元素。任何地方都能調用,是不是so easy~

4.這個框架的實現。

實際上就是使用了系統的present方法,我們做的僅僅只是把present這個動畫自定義了,簡單說一下在寫這個框架過程中需要注意幾個坑,但是在這之前,強烈建議先看看如何自定義轉場動畫,不然會一臉懵逼。
首先如何自定義轉場動畫我們就不多說了,網上有非常多優秀的文章都有說到,可以自行搜索一下,比如這個iOS自定義轉場詳解03總共有4個demo,而且這里面有非常多經典的動畫效果。有興趣可以學習一波。。說幾個在寫功能時候的坑

a、按照流程,寫好動畫~但是在轉場動畫完成的時候,根控制器消失了!!!!

我們的根控制并沒有使用截屏圖放在后面用來做動畫,而是直接將控制器的view放在動畫容器containerView內(為啥不用更方便的截圖來做動畫,是因為仔細看過QQ的側滑,發現在打開菜單的狀態下,QQ右側界面在接收到消息還是會跟著變,所以QQ不是用的截屏圖來做動畫的,于是,它能那樣實現,咱們肯定也是能實現的對吧),在實現的過程中發現這樣會導致在動畫結束的時候,根控制器這邊的view是消失,如下圖:

示例圖2.gif

當時有點沒想明白,看了一些網上大神的資料,有一些用截屏的圖片做動畫的不會有這個問題(但我們已經明確QQ不是用的截屏圖,所以咱們肯定也可以用這個之外的方式解決是吧),還有一些是設置控制器的modalPresentationStyle為UIModalPresentationCustom,設置之后發現顯示側滑的時候正常了,哇,可行,以為OK了,但是當返回之后界面又消失了=。=!就像這樣~


示例圖3.gif

所以這樣也不是特別好,最后我們從源頭想了想,為什么我動畫結束之后view會消失,無非就是幾種情況,1、frame不對,2、透明度為0或者設置為hidden了,3、就是被從父視圖移除了,顯然1,2理論上都不會發生,最終我們在動畫結束的時候打印根控制器view的父視圖發現為nil,找到原因我們就解決它,所以我們在動畫結束之后,又把該view添加到containerView上:

if (![transitionContext transitionWasCancelled]) {
            [transitionContext completeTransition:YES];
            // 動畫完成,再次添加根視圖的view
            [containerView addSubview:fromVC.view];
            maskView.userInteractionEnabled = YES;
        }else {
            [transitionContext completeTransition:NO];
            [imageV removeFromSuperview];
        }

這樣就完全沒問題了~具體為啥動畫結束要被移除我也不是很明白,但是我猜可能是出于內存管理考慮,添加上去之后引用計數又+1不利于界面釋放,所以動畫結束又移除掉了,引用計數-1,讓這個動畫過程不對view的內存產生影響,知道為啥的童鞋可以留言指導一下。在蘋果官方文檔上completeTransition: 這個方法有說明:The default implementation of this method calls the animator object’s animationEnded(:) method to give it a chance to perform any last minute cleanup.(調用completeTransition: 之后 會默認調用animationEnded(:) 方法來進行清理工作,所以我們的猜測應該是正確的,動畫完成就被清理了。。)

b、還有一個細節就是在做手勢驅動結束的時候動畫不連貫。

這個我們要先說一下在手勢過程中,重要的幾個方法

// 手勢過程中,更新轉場執行的進度,參數為所傳的百分比
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
// 調用此方法會取消轉場,并且回到轉場動畫之前的狀態
- (void)cancelInteractiveTransition;
// 手勢結束之后,如果想完成轉場調用此方法
- (void)finishInteractiveTransition;

就是我們在手勢過程中,會傳一個百分比去調用第一個方法,然后一般會設置一個轉場是取消還是完成的臨界點,如果在手勢過程中,超過這個臨界點,我們就調用完成轉場,如果不到這個臨界點,我們就取消,比如我們臨界點設置為轉場完成的50%也就是0.5,那么在手勢過程中超過0.5的時候我們會調用finishInteractiveTransition方法完成轉場,但是轉場動畫的更新會從0.5突然跳到1.0,這時屏幕就會閃一下,甚至不僅僅是閃一下,還會彈幾下再結束動畫,就像這樣:


示例圖4.gif

在手勢過程不到臨界點值時調用取消動畫的時候照樣會出現這個問題。
提供一下我們的解決思路:
啟動一個定時器,將未完成的動畫用一個定時器一步步更新到動畫結束后再調用完成轉場或者取消轉場,也就是說,比如我們現在手勢拖動動畫更新到0.5,這個時候我們松手,如果直接調用完成轉場,界面就會從0.5猛的跳到1.0,中間的這段動畫就會閃過去或者異常,于是我們將0.5-1.0這段時間的更新用一個定時器從0.5跑到1.0再調用完成手勢切換,那么整個轉場動畫的過程就會變的非常流暢~我們定時器用的是CADisplayLink,實現方式如下:

- (void)startDisplayLink:(CGFloat)percent{
    // 首先判斷是完成轉場動畫還是取消轉場動畫
    _toFinish = percent > 0.5;
    // 再根據動畫總時長求得剩下的歸0(取消)或者完成轉場動畫需要執行的時長
    CGFloat remainDuration = _toFinish ? self.duration * (1 - percent) : self.duration * percent;
    // 以每秒60次刷新計算定時器需要執行多少次
    _remaincount = 60 * remainDuration;
    // 算出定時器每執行一次需要改變的動畫百分比
    _oncePercent = _toFinish ? (1 - percent) / _remaincount : percent / _remaincount;
    // 開始定時器,每執行一次定時器,定時器剩余次數-1,當剩余0次時,結束定時器,完成轉場。
    [self starDisplayLink];
}

處理完這些,我們就獲得了一個非常流暢的側滑動畫,很開心~


示例圖5.gif

c、但是。。開心不過3秒。發現由于我們的側滑出來的控制器實際上是present出來的,沒有導航控制器!!!!那我們要在這個控制器里面進行push操作怎么辦??一臉懵逼~

還好,咱們有QQ可以借鑒一下,因為我們大膽的猜測QQ就是present的,所以為啥QQ側滑的控制器可以push呢,在使用中發現,QQ在push的時候應該是先把側邊控制器dismiss,然后再使用根控制器tabbarController的第一個控制器的導航控制器進行push的,也就是說分為兩步,第一步把左側present的控制器dismiss,然后拿到QQ的消息控制器的導航控制器進行push操作,為啥會這么覺得呢,因為使用QQ push出來的控制器再返回pop的時候是直接回到根控制器的!于是我們的push操作這樣去實現它:

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

自己定義一個方法,最終目的就是找到一個最優的導航控制器來進行push操作,所以在使用這個框架的時候,側滑出來的控制器要進行push操作,不能使用系統的push方法(畢竟它沒有導航控制器,就算你打死它它也還是不能push呀),必須使用我寫的這個方法,實現之后push效果如下:

示例圖6.gif

這些細節都處理之后,整個效果基本都沒問題了~所以我們可以開心的去使用這個框架啦~

5.最后

第一次在簡書上寫文章,覺得對你有所幫助的童鞋幫忙點個喜歡(多謝??),想使用這個框架或者想看一下實現原理的童鞋前往我的github地址(很簡單的啦):

一行代碼集成超低耦合的側滑功能

走過路過star不要錯過~感謝。

目前已經支持cocoapods安裝。PS:第一次做支持cocoapods,真是一把心酸一把淚。。成功率在10%不到,加上網速又慢,真是極大的鍛煉了我的耐心。能成功搜索到之后真是淚牛滿面。如果遇到什么問題或者優化建議歡迎留言。。畢竟這是我的第!一!次!!!要踐踏,請溫柔~~。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375