需要側滑抽屜效果?一行程式碼足以!

___發表於2018-01-04

1.為啥要重複造輪子

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

示例圖@2x.png

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

2.未知標題

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

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

示例圖1.gif
scrollView巢狀tableview處理手勢側滑的場景
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安裝,最新版本為1.3.0。PS:第一次做支援cocoapods,真是一把心酸一把淚。。成功率在10%不到,加上網速又慢,真是極大的鍛鍊了我的耐心。能成功搜尋到之後真是淚牛滿面。如果遇到什麼問題或者優化建議歡迎留言提issue。。

相關文章