1. 前言
前段時(shí)間做 Android 開(kāi)發(fā)時(shí)使用系統(tǒng)控件 DrawerLayout 輕松實(shí)現(xiàn)了左右側(cè)欄,最近做 iOS 開(kāi)發(fā)時(shí)恰好需要用到,本想著跟 Android 一樣會(huì)有系統(tǒng)級(jí)的控件提供,誰(shuí)料 Apple 并不提倡使用「?jìng)?cè)欄」的交互模式,未提供相關(guān)控件。由于項(xiàng)目需要用到左右側(cè)欄,擺在面前的只有兩個(gè)選擇:使用第三方開(kāi)源庫(kù),或自己造輪子。
首先,我們先看看使用 Android 系統(tǒng)控件實(shí)現(xiàn)的側(cè)欄效果:

Figure 1.1 : DrawerLayout 實(shí)現(xiàn)效果
從圖1.1中可以看出,DrawerLayout 大致實(shí)現(xiàn)了下述幾種交互:
- 左側(cè)欄未彈出的情況下:
- 點(diǎn)擊左上角 Menu 按鈕,左側(cè)欄彈出,同時(shí)添加黑色半透明 View 對(duì)主界面進(jìn)行遮蓋
- 手指從屏幕左方邊緣向右滑動(dòng),左側(cè)欄相應(yīng)右移,黑色背景 View 的透明度隨右移距離增加而變深;若右劃距離不超過(guò)側(cè)欄寬度一半,手指松開(kāi)后側(cè)欄收回;若右劃距離超過(guò)側(cè)欄一半,則側(cè)欄彈出 。( 此功能在模擬器上未能觸發(fā),需在真機(jī)上重現(xiàn) )
- 左側(cè)欄已彈出的情況下:
- 選中左側(cè)欄中的某個(gè) Item ,左側(cè)欄收回
- 點(diǎn)擊黑色半透明 View,左側(cè)欄收回
- 手指向左滑動(dòng),左側(cè)欄相應(yīng)進(jìn)行左劃,黑色背景 View 的透明度隨左移距離增加而變淺;若左劃距離不超過(guò)側(cè)欄寬度一半,手指松開(kāi)后側(cè)欄回到原位置;若左劃距離超過(guò)側(cè)欄寬度一半,左側(cè)欄收回
在下載嘗試了幾款 iOS 側(cè)欄開(kāi)源庫(kù)后,發(fā)現(xiàn)基本上都不能滿足 DrawerLayout 的相同交互,而且大部分都是使用純代碼實(shí)現(xiàn),與公司 iOS 項(xiàng)目遵循的 storyboard 優(yōu)先原則有所違背,最終決定造個(gè)輪子,讓 iOS 及 Android app 的 UX/UI 盡可能一致。
1.1 開(kāi)發(fā)環(huán)境
- macOS Sierra : 10.12.1 (16B2555)
- Xcode : 8.1 (8B62)
- Objective-C
- Android Studio : 2.2.2
1.2 工具
- Keynote
- GIPHY Capture
- MWeb
- IconJar
- Sketch
1.3 完整工程
Talk is cheap, show me the code!
DrawerLayoutDemo
1.4 最終效果

Figure 1.2 : iOS 頁(yè)面結(jié)構(gòu)

Figure 1.3 : iOS DrawerLayoutDemo 實(shí)現(xiàn)效果
2. 實(shí)現(xiàn)過(guò)程
2.1 思路

Figure 2.1 : DrawerLayout 實(shí)現(xiàn)方式
從 Figure 2.1 中可以看出,在 Activity ( 相當(dāng)于 iOS 的ViewController ) 中,包含了三個(gè) RelativeLayout,分別代表左側(cè)欄、主頁(yè)面和右側(cè)欄,其中左側(cè)欄和右側(cè)欄的默認(rèn)起始坐標(biāo)均處于屏幕可視范圍外,所以對(duì)用戶來(lái)說(shuō),左右側(cè)欄在彈出時(shí)才加載顯示,但事實(shí)上在 Activity 加載時(shí),左右側(cè)欄已經(jīng)加載了,只是顯示位置在屏幕范圍外而已。
同理,在 iOS 中,我們可以在 ViewController 中添加三個(gè) Container View ,分別對(duì)應(yīng)左側(cè)欄、主頁(yè)面和右側(cè)欄,并實(shí)現(xiàn)
- 在
ViewController加載時(shí),將左側(cè)欄和右側(cè)欄的frame均設(shè)置在屏幕范圍外來(lái)達(dá)到側(cè)欄「隱藏」效果 - 在側(cè)欄彈出和收回時(shí),增加頁(yè)面平移動(dòng)畫(huà),實(shí)現(xiàn)彈出和收回的動(dòng)畫(huà)效果
- 在手指滑動(dòng)時(shí),捕捉滑動(dòng)手勢(shì),實(shí)現(xiàn)頁(yè)面隨手指移動(dòng)的動(dòng)畫(huà)效果
- 在
ViewController中,增加backgroundView,使其層級(jí)處于主頁(yè)面view之上,側(cè)欄view之下,實(shí)現(xiàn)黑色半透明背景
2.2 Container View 介紹
Container view controllers are a way to combine the content from multiple view controllers into a single user interface. Container view controllers are most often used to facilitate navigation and to create new user interface types based on existing content.
Examples of container view controllers in UIKit include UINavigationController, UITabBarController, and UISplitViewController, all of which facilitate navigation between different parts of your user interface.
根據(jù)蘋(píng)果的官方介紹,Container View 主要用于將多個(gè)頁(yè)面的內(nèi)容整合到一個(gè)頁(yè)面,同時(shí),每個(gè) Container View 均對(duì)應(yīng)一個(gè)獨(dú)立的 View Controller,將每個(gè) Container View 的功能解耦,避免主 View Controller 過(guò)于臃腫。這樣看來(lái),Container View 用于實(shí)現(xiàn) DrawerLayout 最合適不過(guò)。
2.3 代碼框架搭建

Figure 2.2 : 代碼框架
2.3.1 Storyboard 搭建
根據(jù)圖2.2,我們?cè)?Main.storyboard 中
- 創(chuàng)建一個(gè)
ContainersViewController,作為RootViewController -
ContainersViewController里面放置三個(gè)與屏幕同樣大小的Container View,分別對(duì)應(yīng) LeftMenuViewControllerMainViewControllerRightMenuViewController- 新建一個(gè)與屏幕同樣大小的
View,作為backgroundView,設(shè)置Background = Black Color; Alpha = 0.5
考慮到使用系統(tǒng)內(nèi)建的 Navigation Bar ,以及 MainViewController 里面通常都會(huì)有一些 push navigation 的頁(yè)面跳轉(zhuǎn)需求,故通過(guò) Editor -> Embed in -> Navigation Controller 給 MainViewController 增加一個(gè) Navigation Controller 作為 parent controller ,同理,可使用相同方式添加 Tab Bar Controller 。
2.3.2 代碼目錄搭建
對(duì)應(yīng) Main.storyboard 中的頁(yè)面,新建
- ContainerViewController.h & .m
- MainViewController.h & .m
- LeftMenuViewController.h & .m
- RightMenuViewController.h & .m
做好必要的 AutoLayout 設(shè)置,以及 ViewController 映射后,我們?cè)?ContainerViewController.m 的 viewDidLayoutSubviews 中增加少量代碼,編譯運(yùn)行看看頁(yè)面架構(gòu)是否符合需求。
@interface ContainerViewController ()
@property (weak, nonatomic) IBOutlet UIView *leftMenuContainerView;
@property (weak, nonatomic) IBOutlet UIView *rightMenuContainerView;
@property (weak, nonatomic) IBOutlet UIView *mainContainerView;
@property (weak, nonatomic) IBOutlet UIView *backgroundView;
@end
...
@implementation ContainerViewController
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
/* 測(cè)試代碼,待刪*/
//獲取 ContainersView 的 frame
CGRect windowFrame = self.view.frame;
//將 LeftMenuContainerView 的 x 軸起始坐標(biāo)左移出屏幕左側(cè)
[self.leftMenuContainerView setFrame:CGRectMake (100.0 - windowFrame.size.width, 0, self.leftMenuContainerView.frame.size.width, self.leftMenuContainerView.frame.size.height)];
//將 RightMenuContainerView 的 x 軸起始坐標(biāo)右移到屏幕右側(cè)左方
[self.rightMenuContainerView setFrame:CGRectMake (windowFrame.size.width - 100.0, 0, self.rightMenuContainerView.frame.size.width, self.rightMenuContainerView.frame.size.height)];
//將 backGroundView 顏色設(shè)置為黑色,透明度設(shè)置為 50%
[self.backgroundView setBackgroundColor:[UIColor blackColor]];
[self.backgroundView setAlpha:0.5];
/* 測(cè)試代碼,待刪*/
}
@end
運(yùn)行效果如下:

Figure 2.3 : 代碼框架運(yùn)行效果
2.4 ViewController 代碼實(shí)現(xiàn)
對(duì)章節(jié) 1 中描述的交互需求進(jìn)行分解,我們可以得到每個(gè) ViewController 需要實(shí)現(xiàn)的功能
ContainersViewController- 頁(yè)面初始化時(shí)設(shè)置
LeftMenuContainerView及RightMenuContainerView的初始位置為屏幕兩側(cè);backgroundView的默認(rèn)狀態(tài)為alpha = 0.0, hidden = YES -
LeftMenuContainerView彈出及收回 -
RightMenuContainerView彈出及收回 - 左側(cè)屏幕邊緣滑入手勢(shì)捕捉,
LeftMenuContainerView隨手勢(shì)在 x 軸上平移,松手時(shí)判斷需彈出或收回;backgroundVIew透明度隨手勢(shì)漸變 - 右側(cè)屏幕邊緣滑入手勢(shì)捕捉,
RightMenuContainerView隨手勢(shì)在 x 軸上平移,松手時(shí)判斷需彈出或收回;backgroundVIew透明度隨手勢(shì)漸變 - 當(dāng)
LeftMenuContainerView已彈出時(shí),屏蔽右側(cè)屏幕邊緣滑入手勢(shì)捕捉,收回后重新開(kāi)啟;同理,RightMenuContainerView彈出后,屏蔽左側(cè)屏幕邊緣滑入手勢(shì)捕捉,收回后重新開(kāi)啟 MainViewController- 點(diǎn)擊
Navigation Bar上的LeftMenu按鈕后,「通知」ContainersViewController彈出左側(cè)欄 - 點(diǎn)擊
Navigation Bar上的RightMenu按鈕后,「通知」ContainersViewController彈出右側(cè)欄 LeftMenuViewController-
View的右側(cè)設(shè)置一個(gè)全透明的transparentView,用于「透視」ContainersViewController上的backgroundView - 點(diǎn)擊右側(cè)的
transparentView,「通知」ContainersViewController收回左側(cè)欄 - 點(diǎn)擊
LeftMenuViewController上的「項(xiàng)目」,「通知」ContainersViewController收回左側(cè)欄 - 捕捉滑動(dòng)手勢(shì),
LeftMenuViewController隨手勢(shì)在 x 軸上平移,松手時(shí)判斷需恢復(fù)到彈出狀態(tài),還是通知ContainersViewController收回左側(cè)欄 RightMenuViewController-
View的左側(cè)設(shè)置一個(gè)全透明的transparentView,用于「透視」ContainersViewController上的backgroundView - 點(diǎn)擊左側(cè)的
transparentView,「通知」ContainersViewController收回右側(cè)欄 - 點(diǎn)擊
RightMenuViewController上的「項(xiàng)目」,「通知」ContainersViewController收回右側(cè)欄 - 捕捉滑動(dòng)手勢(shì),
RightMenuViewController隨手勢(shì)在 x 軸上平移,松手時(shí)判斷需恢復(fù)到彈出狀態(tài),還是通知ContainersViewController收回右側(cè)欄
講到這里,相信大家都可以明顯地感受到
Container View的好處。通過(guò)使用Container View對(duì)ViewController的功能進(jìn)行解耦,在避免產(chǎn)生單個(gè)臃腫ViewController的同時(shí),又能很好地實(shí)現(xiàn)復(fù)雜的單頁(yè)面功能;同時(shí)對(duì)多尺寸、橫豎屏的適配也更靈活方便,推薦大家多使用。
這里插播一下,上面功能分析提到的「通知」,有很多種實(shí)現(xiàn)方式,包括但不限于 NSNotificationCenter ,Delegate ,函數(shù)調(diào)用 。本教程的「通知」使用的是 函數(shù)調(diào)用 的方式。
「萬(wàn)事俱備,只欠東風(fēng)」,功能分解完畢,接下來(lái)只需逐個(gè)擊破!
2.5 ContainersViewController
首先,記得將章節(jié) 2.3.2 中的測(cè)試代碼刪除。
- 頁(yè)面初始化時(shí)設(shè)置
LeftMenuContainerView及RightMenuContainerView的初始位置為屏幕兩側(cè);backgroundView的默認(rèn)狀態(tài)為alpha = 0.0, hidden = YES
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self viewItemInitial];
}
//... other functions ...
- (void)viewItemInitial {
//設(shè)置 backgroundView 初始隱藏狀態(tài)及透明度
[self.backgroundView setAlpha:0.0];
[self.backgroundView setHidden:YES];
CGRect windowFrame = self.view.frame;
CGFloat startX = 0.0;
//設(shè)置 leftMenuContainerView 初始位置
startX = -windowFrame.size.width;
[self.leftMenuContainerView setFrame:CGRectMake(startX,
self.leftMenuContainerView.frame.origin.y,
self.leftMenuContainerView.frame.size.width,
self.leftMenuContainerView.frame.size.height)];
//設(shè)置 rightMenuContainerView 初始位置
startX = windowFrame.size.width;
[self.rightMenuContainerView setFrame:CGRectMake(startX,
self.rightMenuContainerView.frame.origin.y,
self.rightMenuContainerView.frame.size.width,
self.rightMenuContainerView.frame.size.height)];
}
-
backgroundView漸隱及漸顯動(dòng)畫(huà);需暴露接口供其他ViewController使用
- (void) showBackgroundView {
[self.backgroundView setHidden:NO];
[UIView animateWithDuration:self.bgViewAnimationDuration animations:^{
[self.backgroundView setAlpha:self.bgViewFinalAlpha];
}];
}
- (void) dismissBackgroundView {
[UIView animateWithDuration:self.bgViewAnimationDuration animations:^{
[self.backgroundView setAlpha:0.0];
} completion:^(BOOL finished) {
[self.backgroundView setHidden:YES];
}];
}
-
LeftMenuContainerView彈出及收回;RightMenuContainerView彈出及收回動(dòng)畫(huà);需暴露接口供其他ViewController使用
#pragma public function
- (void)showLeftMenu {
[self showMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
}
- (void)dismissLeftMenu {
[self dismissMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
}
- (void)showRightMenu {
[self showMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
}
- (void)dismissRightMenu {
[self dismissMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
}
#pragma private function
- (void)showMenu:(UIView *) view menuType:(MENU_TYPE) menuType {
CGFloat finalX = 0.0;
if (menuType == MENU_TYPE_UNKNOWN) {
return;
}
[UIView animateWithDuration:self.menuAnimationDuration animations:^{
[view setFrame:CGRectMake(finalX,
view.frame.origin.y,
view.frame.size.width,
view.frame.size.height)];
}];
}
- (void)dismissMenu:(UIView *) view menuType:(MENU_TYPE) menuType {
CGRect windowFrame = self.view.frame;
CGFloat finalX = 0.0;
if (menuType == MENU_TYPE_LEFT_MENU) {
finalX = 0 - windowFrame.size.width;
}
else if (menuType == MENU_TYPE_RIGHT_MENU) {
finalX = windowFrame.size.width;
}
else {
return;
}
[UIView animateWithDuration:self.menuAnimationDuration animations:^{
[view setFrame:CGRectMake(finalX,
view.frame.origin.y,
view.frame.size.width,
view.frame.size.height)];
}];
}
- 雙側(cè)屏幕邊緣滑入手勢(shì)捕捉,
LeftMenuContainerView或RightMenuContainerView隨手勢(shì)在 x 軸上平移,松手時(shí)判斷需彈出或收回;backgroundVIew透明度隨手勢(shì)漸變
- (void)gestureRecognizerInitial {
self.screenEdgePanGestureRecognizerLeft = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgePanGestureRecognizerHandler:)];
[self.screenEdgePanGestureRecognizerLeft setEdges:UIRectEdgeLeft];
self.screenEdgePanGestureRecognizerRight = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgePanGestureRecognizerHandler:)];
[self.screenEdgePanGestureRecognizerRight setEdges:UIRectEdgeRight];
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerLeft];
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerRight];
}
- (void)screenEdgePanGestureRecognizerHandler:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
if ((gestureRecognizer.edges == UIRectEdgeLeft) || (gestureRecognizer.edges == UIRectEdgeRight)) {
//獲取手指相對(duì)于屏幕的坐標(biāo)
CGPoint gesturePoint = [gestureRecognizer locationInView:self.view];
CGFloat windowWidth = self.view.frame.size.width;
//滑動(dòng)開(kāi)始,保存初始坐標(biāo)
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
self.panGestureStartPointX = gesturePoint.x;
[self.backgroundView setHidden:NO];
}
//滑動(dòng)過(guò)程中,動(dòng)態(tài)改變 menuView 位置及 backgroundView 透明度
else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
CGFloat deltaX = 0;
//計(jì)算手指相對(duì)起始位置的滑動(dòng)距離
deltaX = (gestureRecognizer.edges == UIRectEdgeLeft) ?
(gesturePoint.x - self.panGestureStartPointX) : (self.panGestureStartPointX - gesturePoint.x);
//如果滑動(dòng)距離是負(fù)數(shù),則說(shuō)明手指滑動(dòng)方向與側(cè)欄彈出反向相反,無(wú)需處理
if (deltaX > 0.0) {
CGFloat newPointX = 0.0;
CGFloat newBgAlpha = 0.0;
UIView *menuView = nil;
if (gestureRecognizer.edges == UIRectEdgeLeft) {
newPointX = -windowWidth + deltaX;
newBgAlpha = (newPointX + windowWidth) / windowWidth * self.bgViewFinalAlpha;
menuView = self.leftMenuContainerView;
}
else {
newPointX = windowWidth - deltaX;
newBgAlpha = (windowWidth - newPointX) / windowWidth * self.bgViewFinalAlpha;
menuView = self.rightMenuContainerView;
}
//更新 menuView 顯示位置
[menuView setFrame:CGRectMake(newPointX, menuView.frame.origin.y, menuView.frame.size.width, menuView.frame.size.height)];
//更新 backgroundView 透明度
[self.backgroundView setAlpha:newBgAlpha];
}
}
//滑動(dòng)結(jié)束后,判斷該彈出還是收回 menuView
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//計(jì)算 menuView 的最終位移
CGFloat viewOffset = (gestureRecognizer.edges == UIRectEdgeLeft) ?
(self.leftMenuContainerView.frame.origin.x + windowWidth) : (windowWidth - self.rightMenuContainerView.frame.origin.x);
//彈出/收回側(cè)欄
if (viewOffset > self.minOffset) {
(gestureRecognizer.edges == UIRectEdgeLeft) ? ([self showLeftMenu]) : ([self showRightMenu]);
[self showBackgroundView];
}
else {
(gestureRecognizer.edges == UIRectEdgeLeft) ? ([self dismissLeftMenu]) : ([self dismissRightMenu]);
[self dismissBackgroundView];
}
}
}
}
- 當(dāng)
LeftMenuContainerView已彈出時(shí),屏蔽右側(cè)屏幕邊緣滑入手勢(shì)捕捉,收回后重新開(kāi)啟;同理,RightMenuContainerView彈出后,屏蔽左側(cè)屏幕邊緣滑入手勢(shì)捕捉,收回后重新開(kāi)啟
使用 addGestureRecognizer 和 removeGestureRecognizer ,在彈出/收回側(cè)欄時(shí)對(duì)手勢(shì)捕捉進(jìn)行使能/禁止
- (void)enableEdgePanGestureRecognizer {
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerLeft];
[self.view addGestureRecognizer:self.screenEdgePanGestureRecognizerRight];
}
- (void)disableEdgePanGestureRecognizer {
[self.view removeGestureRecognizer:self.screenEdgePanGestureRecognizerLeft];
[self.view removeGestureRecognizer:self.screenEdgePanGestureRecognizerRight];
}
- (void)showLeftMenu {
[self showMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
[self disableEdgePanGestureRecognizer];
}
- (void)dismissLeftMenu {
[self dismissMenu:self.leftMenuContainerView menuType:MENU_TYPE_LEFT_MENU];
[self enableEdgePanGestureRecognizer];
}
- (void)showRightMenu {
[self showMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
[self disableEdgePanGestureRecognizer];
}
- (void)dismissRightMenu {
[self dismissMenu:self.rightMenuContainerView menuType:MENU_TYPE_RIGHT_MENU];
[self enableEdgePanGestureRecognizer];
}
2.6 MainViewController
MainViewController 只做兩件事情,「通知」ContainersViewController 彈出/收回 LeftMenuContainerView/RightMenuContainerView
2.6.1 storyboard 實(shí)現(xiàn)
添加 LeftMenu 和 RightMenu 兩個(gè) Bar Button Item 到 Navigation Bar ,并將 Button Action 關(guān)聯(lián)到 MainViewController 中。

Figure 2.4 : MainViewController 頁(yè)面
2.6.2 代碼實(shí)現(xiàn)
在章節(jié) 2.4 中提到,本 demo 中「通知」的方式使用的是函數(shù)調(diào)用,所以在 MainViewController 中,當(dāng)用戶點(diǎn)擊 LeftMenu 和 RightMenu Button時(shí),需要通過(guò)調(diào)用 ContainersViewController 暴露出來(lái)的函數(shù)實(shí)現(xiàn)左右側(cè)欄的顯示。
從 storyboard 中可知, self.parentViewController 獲取到的是 navigationController ,self.parentViewController.parentViewController 獲取到的便是 ContainersViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.containerViewController = (ContainerViewController *) self.parentViewController.parentViewController;
}
- (IBAction)leftMenuButtonAction:(UIBarButtonItem *)sender {
[self.containerViewController showLeftMenu];
[self.containerViewController showBackgroundView];
}
- (IBAction)rightMenuButtonAction:(UIBarButtonItem *)sender {
[self.containerViewController showRightMenu];
[self.containerViewController showBackgroundView];
}
2.6.3 LeftMenuViewController
-
View的右側(cè)設(shè)置一個(gè)全透明的transparentView,用于「透視」ContainersViewController上的backgroundView
Figure 2.5 : LeftMenuViewController 頁(yè)面
- 點(diǎn)擊右側(cè)的
transparentView,「通知」ContainersViewController收回左側(cè)欄;點(diǎn)擊LeftMenuViewController上的「項(xiàng)目」,「通知」ContainersViewController收回左側(cè)欄
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.containerViewController = (ContainerViewController *)self.parentViewController;
[self gestureRecognizerInitial];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)gestureRecognizerInitial {
UITapGestureRecognizer *transparentViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(transparentViewTapHandler:)];
[self.transparentView addGestureRecognizer:transparentViewTapGestureRecognizer];
UITapGestureRecognizer *bookViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(transparentViewTapHandler:)];
[self.booksView addGestureRecognizer:bookViewTapGestureRecognizer];
UITapGestureRecognizer *tagViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(transparentViewTapHandler:)];
[self.tagView addGestureRecognizer:tagViewTapGestureRecognizer];
}
- (void)transparentViewTapHandler:(UITapGestureRecognizer *)gestureRecognizer {
[self.containerViewController dismissLeftMenu];
[self.containerViewController dismissBackgroundView];
}
- 捕捉滑動(dòng)手勢(shì),
LeftMenuViewController隨手勢(shì)在 x 軸上平移,松手時(shí)判斷需恢復(fù)到彈出狀態(tài),還是通知ContainersViewController收回左側(cè)欄
- (void)gestureRecognizerInitial {
......
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognizerHandler:)];
[self.view addGestureRecognizer:panGestureRecognizer];
}
- (void)panGestureRecognizerHandler:(UIPanGestureRecognizer *)gestureRecognizer {
//獲取手指相對(duì)于屏幕的坐標(biāo)
CGPoint gesturePoint = [gestureRecognizer locationInView:self.containerViewController.view];
CGFloat windowWidth = self.containerViewController.view.frame.size.width;
//滑動(dòng)開(kāi)始,保存初始坐標(biāo)
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
self.panGestureStartPointX = gesturePoint.x;
}
//滑動(dòng)過(guò)程中,動(dòng)態(tài)改變 menuView 位置及 backgroundView 透明度
else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
CGFloat deltaX = 0;
//計(jì)算手指相對(duì)起始位置的滑動(dòng)距離
deltaX = self.panGestureStartPointX - gesturePoint.x;
//如果滑動(dòng)距離是負(fù)數(shù),則說(shuō)明手指滑動(dòng)方向與側(cè)欄回收方向相反,無(wú)需處理
if (deltaX > 0.0) {
CGFloat newPointX = 0.0;
CGFloat newBgAlpha = 0.0;
CGFloat bgViewFinalAlpha = [self.containerViewController getBgViewFinalAlphaValue];
newPointX = -deltaX;
newBgAlpha = (newPointX + windowWidth) / windowWidth * bgViewFinalAlpha;
//更新 menuView 顯示位置
CGRect newFrame = CGRectMake(newPointX, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
[self.containerViewController modifyMenuViewFrame:newFrame menuType:MENU_TYPE_LEFT_MENU];
//更新 backgroundView 透明度
[self.containerViewController modifyBackgroundViewAlpha:newBgAlpha];
}
}
//滑動(dòng)結(jié)束后,判斷該彈出還是收回 menuView
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
CGFloat minOffset = [self.containerViewController getMinOffset];
//計(jì)算 menuView 的最終位移
CGFloat viewOffset = -self.containerViewController.leftMenuContainerView.frame.origin.x;
//彈出/收回側(cè)欄
if (viewOffset > minOffset) {
[self.containerViewController dismissLeftMenu];
[self.containerViewController dismissBackgroundView];
}
else {
[self.containerViewController showLeftMenu];
[self.containerViewController showBackgroundView];
}
}
}
2.6.4 RightMenuViewController
實(shí)現(xiàn)方式與 LeftMenuViewController 相同,只是在拖拽手勢(shì)處理時(shí)坐標(biāo)計(jì)算有少許變化。
- (void)panGestureRecognizerHandler:(UIPanGestureRecognizer *)gestureRecognizer {
//獲取手指相對(duì)于屏幕的坐標(biāo)
CGPoint gesturePoint = [gestureRecognizer locationInView:self.containerViewController.view];
CGFloat windowWidth = self.containerViewController.view.frame.size.width;
//滑動(dòng)開(kāi)始,保存初始坐標(biāo)
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
self.panGestureStartPointX = gesturePoint.x;
}
//滑動(dòng)過(guò)程中,動(dòng)態(tài)改變 menuView 位置及 backgroundView 透明度
else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
CGFloat deltaX = 0;
//計(jì)算手指相對(duì)起始位置的滑動(dòng)距離
deltaX = gesturePoint.x - self.panGestureStartPointX;
//如果滑動(dòng)距離是負(fù)數(shù),則說(shuō)明手指滑動(dòng)方向與側(cè)欄回收方向相反,無(wú)需處理
if (deltaX > 0.0) {
CGFloat newPointX = 0.0;
CGFloat newBgAlpha = 0.0;
CGFloat bgViewFinalAlpha = [self.containerViewController getBgViewFinalAlphaValue];
newPointX = deltaX;
newBgAlpha = (windowWidth - newPointX) / windowWidth * bgViewFinalAlpha;
//更新 menuView 顯示位置
CGRect newFrame = CGRectMake(newPointX, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
[self.containerViewController modifyMenuViewFrame:newFrame menuType:MENU_TYPE_RIGHT_MENU];
//更新 backgroundView 透明度
[self.containerViewController modifyBackgroundViewAlpha:newBgAlpha];
}
}
//滑動(dòng)結(jié)束后,判斷該彈出還是收回 menuView
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
CGFloat minOffset = [self.containerViewController getMinOffset];
//計(jì)算 menuView 的最終位移
CGFloat viewOffset = self.containerViewController.rightMenuContainerView.frame.origin.x;
//彈出/收回側(cè)欄
if (viewOffset > minOffset) {
[self.containerViewController dismissRightMenu];
[self.containerViewController dismissBackgroundView];
}
else {
[self.containerViewController showRightMenu];
[self.containerViewController showBackgroundView];
}
}
}
寫(xiě)了這么多,終于接近尾聲!
3. 坑!
-
章節(jié) 2.6.3 & 2.6.4,為何要在
MenuViewController中設(shè)計(jì)transparentView用于「透視」ContainersViewController的黑色半透明背景,而不直接將MenuContainerView的寬度固定為有效內(nèi)容寬度,而非全屏幕?- 假設(shè)
MenuViewController寬度不是全屏幕,但使用了NavigationController,在調(diào)用pushViewController后,新頁(yè)面寬度將和MenuViewController一致,不能全屏顯示。所以這個(gè)地方的實(shí)現(xiàn)邏輯需要根據(jù)項(xiàng)目實(shí)際需求修改。
- 假設(shè)
-
若
MainViewController中存在Scroll View,屏幕邊緣滑入不能觸發(fā)側(cè)欄打開(kāi)- 在
MainViewController中調(diào)用下述代碼,讓ContainersViewController的手勢(shì)優(yōu)先級(jí)更高
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.containersViewController.screenEdgePanGesture];
- 在
若
MainViewController,LeftMenuViewController或RightMenuViewController存在頁(yè)面跳轉(zhuǎn),在跳轉(zhuǎn)后必須禁止ContainersViewController的UIScreenEdgePanGestureRecognizer,否則頁(yè)面跳轉(zhuǎn)后仍能通過(guò)屏幕邊緣滑入手勢(shì)彈出側(cè)欄如需要同時(shí)使用
TabBarController,只需在MainViewController的NavigationController前添加一個(gè)TabBarController即可
4. 寫(xiě)在最后
對(duì)于雖說(shuō)側(cè)欄只是一個(gè)很舊的,甚至不被蘋(píng)果提倡的功能,不過(guò)通過(guò)這次「造」輪子,也算比較深入地了解了 Container View ,獲益匪淺。
