iOS — 使用 Container View 實現左右側欄


1. 前言

前段時間做 Android 開發時使用系統控件 DrawerLayout 輕松實現了左右側欄,最近做 iOS 開發時恰好需要用到,本想著跟 Android 一樣會有系統級的控件提供,誰料 Apple 并不提倡使用「側欄」的交互模式,未提供相關控件。由于項目需要用到左右側欄,擺在面前的只有兩個選擇:使用第三方開源庫,或自己造輪子。
首先,我們先看看使用 Android 系統控件實現的側欄效果:

Figure 1.1 : DrawerLayout 實現效果

從圖1.1中可以看出,DrawerLayout 大致實現了下述幾種交互:

  • 左側欄未彈出的情況下:
  • 點擊左上角 Menu 按鈕,左側欄彈出,同時添加黑色半透明 View 對主界面進行遮蓋
  • 手指從屏幕左方邊緣向右滑動,左側欄相應右移,黑色背景 View 的透明度隨右移距離增加而變深;若右劃距離不超過側欄寬度一半,手指松開后側欄收回;若右劃距離超過側欄一半,則側欄彈出 。( 此功能在模擬器上未能觸發,需在真機上重現
  • 左側欄已彈出的情況下:
  • 選中左側欄中的某個 Item ,左側欄收回
  • 點擊黑色半透明 View,左側欄收回
  • 手指向左滑動,左側欄相應進行左劃,黑色背景 View 的透明度隨左移距離增加而變淺;若左劃距離不超過側欄寬度一半,手指松開后側欄回到原位置;若左劃距離超過側欄寬度一半,左側欄收回

在下載嘗試了幾款 iOS 側欄開源庫后,發現基本上都不能滿足 DrawerLayout 的相同交互,而且大部分都是使用純代碼實現,與公司 iOS 項目遵循的 storyboard 優先原則有所違背,最終決定造個輪子,讓 iOS 及 Android app 的 UX/UI 盡可能一致。

1.1 開發環境

  • 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 頁面結構

Figure 1.3 : iOS DrawerLayoutDemo 實現效果

2. 實現過程

2.1 思路

Figure 2.1 : DrawerLayout 實現方式

從 Figure 2.1 中可以看出,在 Activity相當于 iOS 的ViewController ) 中,包含了三個 RelativeLayout,分別代表左側欄、主頁面和右側欄,其中左側欄和右側欄的默認起始坐標均處于屏幕可視范圍外,所以對用戶來說,左右側欄在彈出時才加載顯示,但事實上在 Activity 加載時,左右側欄已經加載了,只是顯示位置在屏幕范圍外而已。

同理,在 iOS 中,我們可以在 ViewController 中添加三個 Container View ,分別對應左側欄、主頁面和右側欄,并實現

  • ViewController 加載時,將左側欄和右側欄的 frame 均設置在屏幕范圍外來達到側欄「隱藏」效果
  • 在側欄彈出和收回時,增加頁面平移動畫,實現彈出和收回的動畫效果
  • 在手指滑動時,捕捉滑動手勢,實現頁面隨手指移動的動畫效果
  • ViewController 中,增加 backgroundView,使其層級處于主頁面 view 之上,側欄 view 之下,實現黑色半透明背景

2.2 Container View 介紹

蘋果 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.

根據蘋果的官方介紹,Container View 主要用于將多個頁面的內容整合到一個頁面,同時,每個 Container View 均對應一個獨立的 View Controller,將每個 Container View 的功能解耦,避免主 View Controller 過于臃腫。這樣看來,Container View 用于實現 DrawerLayout 最合適不過。

2.3 代碼框架搭建

Figure 2.2 : 代碼框架

2.3.1 Storyboard 搭建

根據圖2.2,我們在 Main.storyboard

  • 創建一個 ContainersViewController ,作為 RootViewController
  • ContainersViewController 里面放置三個與屏幕同樣大小的 Container View ,分別對應
  • LeftMenuViewController
  • MainViewController
  • RightMenuViewController
  • 新建一個與屏幕同樣大小的 View,作為 backgroundView,設置 Background = Black Color; Alpha = 0.5

考慮到使用系統內建的 Navigation Bar ,以及 MainViewController 里面通常都會有一些 push navigation 的頁面跳轉需求,故通過 Editor -> Embed in -> Navigation ControllerMainViewController 增加一個 Navigation Controller 作為 parent controller ,同理,可使用相同方式添加 Tab Bar Controller

2.3.2 代碼目錄搭建

對應 Main.storyboard 中的頁面,新建

  • ContainerViewController.h & .m
  • MainViewController.h & .m
  • LeftMenuViewController.h & .m
  • RightMenuViewController.h & .m

做好必要的 AutoLayout 設置,以及 ViewController 映射后,我們在 ContainerViewController.mviewDidLayoutSubviews 中增加少量代碼,編譯運行看看頁面架構是否符合需求。

@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];
    
    /* 測試代碼,待刪*/
    //獲取 ContainersView 的 frame
    CGRect windowFrame = self.view.frame;
    
    //將 LeftMenuContainerView 的 x 軸起始坐標左移出屏幕左側
    [self.leftMenuContainerView setFrame:CGRectMake (100.0 - windowFrame.size.width, 0, self.leftMenuContainerView.frame.size.width, self.leftMenuContainerView.frame.size.height)];
    //將 RightMenuContainerView 的 x 軸起始坐標右移到屏幕右側左方
    [self.rightMenuContainerView setFrame:CGRectMake (windowFrame.size.width - 100.0, 0, self.rightMenuContainerView.frame.size.width, self.rightMenuContainerView.frame.size.height)];
    //將 backGroundView 顏色設置為黑色,透明度設置為 50%
    [self.backgroundView setBackgroundColor:[UIColor blackColor]];
    [self.backgroundView setAlpha:0.5];
    /* 測試代碼,待刪*/
}

@end

運行效果如下:

Figure 2.3 : 代碼框架運行效果

2.4 ViewController 代碼實現

對章節 1 中描述的交互需求進行分解,我們可以得到每個 ViewController 需要實現的功能

  • ContainersViewController
  • 頁面初始化時設置 LeftMenuContainerViewRightMenuContainerView 的初始位置為屏幕兩側;backgroundView 的默認狀態為 alpha = 0.0, hidden = YES
  • LeftMenuContainerView 彈出及收回
  • RightMenuContainerView 彈出及收回
  • 左側屏幕邊緣滑入手勢捕捉,LeftMenuContainerView 隨手勢在 x 軸上平移,松手時判斷需彈出或收回;backgroundVIew 透明度隨手勢漸變
  • 右側屏幕邊緣滑入手勢捕捉,RightMenuContainerView 隨手勢在 x 軸上平移,松手時判斷需彈出或收回;backgroundVIew 透明度隨手勢漸變
  • LeftMenuContainerView 已彈出時,屏蔽右側屏幕邊緣滑入手勢捕捉,收回后重新開啟;同理,RightMenuContainerView 彈出后,屏蔽左側屏幕邊緣滑入手勢捕捉,收回后重新開啟
  • MainViewController
  • 點擊 Navigation Bar 上的 LeftMenu 按鈕后,「通知」ContainersViewController 彈出左側欄
  • 點擊 Navigation Bar 上的 RightMenu 按鈕后,「通知」ContainersViewController 彈出右側欄
  • LeftMenuViewController
  • View 的右側設置一個全透明的 transparentView ,用于「透視」 ContainersViewController 上的 backgroundView
  • 點擊右側的 transparentView,「通知」 ContainersViewController 收回左側欄
  • 點擊 LeftMenuViewController 上的「項目」,「通知」 ContainersViewController 收回左側欄
  • 捕捉滑動手勢,LeftMenuViewController 隨手勢在 x 軸上平移,松手時判斷需恢復到彈出狀態,還是通知 ContainersViewController 收回左側欄
  • RightMenuViewController
  • View 的左側設置一個全透明的 transparentView ,用于「透視」 ContainersViewController 上的 backgroundView
  • 點擊左側的 transparentView,「通知」 ContainersViewController 收回右側欄
  • 點擊 RightMenuViewController 上的「項目」,「通知」 ContainersViewController 收回右側欄
  • 捕捉滑動手勢,RightMenuViewController 隨手勢在 x 軸上平移,松手時判斷需恢復到彈出狀態,還是通知 ContainersViewController 收回右側欄

講到這里,相信大家都可以明顯地感受到 Container View 的好處。通過使用 Container ViewViewController 的功能進行解耦,在避免產生單個臃腫 ViewController 的同時,又能很好地實現復雜的單頁面功能;同時對多尺寸、橫豎屏的適配也更靈活方便,推薦大家多使用。

這里插播一下,上面功能分析提到的「通知」,有很多種實現方式,包括但不限于 NSNotificationCenterDelegate函數調用 。本教程的「通知」使用的是 函數調用 的方式。

「萬事俱備,只欠東風」,功能分解完畢,接下來只需逐個擊破!

2.5 ContainersViewController

首先,記得將章節 2.3.2 中的測試代碼刪除。

  • 頁面初始化時設置 LeftMenuContainerViewRightMenuContainerView 的初始位置為屏幕兩側;backgroundView 的默認狀態為 alpha = 0.0, hidden = YES
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    
    [self viewItemInitial];
}

//... other functions ...

- (void)viewItemInitial {
    //設置 backgroundView 初始隱藏狀態及透明度
    [self.backgroundView setAlpha:0.0];
    [self.backgroundView setHidden:YES];
    
    CGRect windowFrame = self.view.frame;
    CGFloat startX = 0.0;
    //設置 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)];
    
    //設置 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 漸隱及漸顯動畫;需暴露接口供其他 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 彈出及收回動畫;需暴露接口供其他 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)];
    }];
}
  • 雙側屏幕邊緣滑入手勢捕捉,LeftMenuContainerViewRightMenuContainerView 隨手勢在 x 軸上平移,松手時判斷需彈出或收回;backgroundVIew 透明度隨手勢漸變
- (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)) {
        //獲取手指相對于屏幕的坐標
        CGPoint gesturePoint = [gestureRecognizer locationInView:self.view];
        CGFloat windowWidth = self.view.frame.size.width;
        
        //滑動開始,保存初始坐標
        if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
            self.panGestureStartPointX = gesturePoint.x;
            [self.backgroundView setHidden:NO];
        }
        //滑動過程中,動態改變 menuView 位置及 backgroundView 透明度
        else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
            CGFloat deltaX = 0;
            
            //計算手指相對起始位置的滑動距離
            deltaX = (gestureRecognizer.edges == UIRectEdgeLeft) ?
                    (gesturePoint.x - self.panGestureStartPointX) : (self.panGestureStartPointX - gesturePoint.x);
            
            //如果滑動距離是負數,則說明手指滑動方向與側欄彈出反向相反,無需處理
            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];
            }
        }
        //滑動結束后,判斷該彈出還是收回 menuView
        else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
            //計算 menuView 的最終位移
            CGFloat viewOffset = (gestureRecognizer.edges == UIRectEdgeLeft) ?
                                (self.leftMenuContainerView.frame.origin.x + windowWidth) : (windowWidth - self.rightMenuContainerView.frame.origin.x);
            //彈出/收回側欄
            if (viewOffset > self.minOffset) {
                (gestureRecognizer.edges == UIRectEdgeLeft) ? ([self showLeftMenu]) : ([self showRightMenu]);
                [self showBackgroundView];
            }
            else {
                (gestureRecognizer.edges == UIRectEdgeLeft) ? ([self dismissLeftMenu]) : ([self dismissRightMenu]);
                [self dismissBackgroundView];
            }
        }
    }
}
  • LeftMenuContainerView 已彈出時,屏蔽右側屏幕邊緣滑入手勢捕捉,收回后重新開啟;同理,RightMenuContainerView 彈出后,屏蔽左側屏幕邊緣滑入手勢捕捉,收回后重新開啟

使用 addGestureRecognizerremoveGestureRecognizer ,在彈出/收回側欄時對手勢捕捉進行使能/禁止

- (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 實現

添加 LeftMenuRightMenu 兩個 Bar Button ItemNavigation Bar ,并將 Button Action 關聯到 MainViewController 中。

Figure 2.4 : MainViewController 頁面

2.6.2 代碼實現

在章節 2.4 中提到,本 demo 中「通知」的方式使用的是函數調用,所以在 MainViewController 中,當用戶點擊 LeftMenuRightMenu Button時,需要通過調用 ContainersViewController 暴露出來的函數實現左右側欄的顯示。

storyboard 中可知, self.parentViewController 獲取到的是 navigationControllerself.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 的右側設置一個全透明的 transparentView ,用于「透視」 ContainersViewController 上的 backgroundView

Figure 2.5 : LeftMenuViewController 頁面

  • 點擊右側的 transparentView,「通知」 ContainersViewController 收回左側欄;點擊 LeftMenuViewController 上的「項目」,「通知」 ContainersViewController 收回左側欄
- (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];
}

  • 捕捉滑動手勢,LeftMenuViewController 隨手勢在 x 軸上平移,松手時判斷需恢復到彈出狀態,還是通知 ContainersViewController 收回左側欄
- (void)gestureRecognizerInitial {
    ......
    UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognizerHandler:)];
    [self.view addGestureRecognizer:panGestureRecognizer];
}

- (void)panGestureRecognizerHandler:(UIPanGestureRecognizer *)gestureRecognizer {
    //獲取手指相對于屏幕的坐標
    CGPoint gesturePoint = [gestureRecognizer locationInView:self.containerViewController.view];
    CGFloat windowWidth = self.containerViewController.view.frame.size.width;
    
    //滑動開始,保存初始坐標
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        self.panGestureStartPointX = gesturePoint.x;
    }
    //滑動過程中,動態改變 menuView 位置及 backgroundView 透明度
    else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
        CGFloat deltaX = 0;
        
        //計算手指相對起始位置的滑動距離
        deltaX = self.panGestureStartPointX - gesturePoint.x;
        
        //如果滑動距離是負數,則說明手指滑動方向與側欄回收方向相反,無需處理
        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];
        }
    }
    //滑動結束后,判斷該彈出還是收回 menuView
    else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        CGFloat minOffset = [self.containerViewController getMinOffset];
        //計算 menuView 的最終位移
        CGFloat viewOffset = -self.containerViewController.leftMenuContainerView.frame.origin.x;
        //彈出/收回側欄
        if (viewOffset > minOffset) {
            [self.containerViewController dismissLeftMenu];
            [self.containerViewController dismissBackgroundView];
        }
        else {
            [self.containerViewController showLeftMenu];
            [self.containerViewController showBackgroundView];
        }
    }
}

2.6.4 RightMenuViewController

實現方式與 LeftMenuViewController 相同,只是在拖拽手勢處理時坐標計算有少許變化。

- (void)panGestureRecognizerHandler:(UIPanGestureRecognizer *)gestureRecognizer {
    //獲取手指相對于屏幕的坐標
    CGPoint gesturePoint = [gestureRecognizer locationInView:self.containerViewController.view];
    CGFloat windowWidth = self.containerViewController.view.frame.size.width;
    
    //滑動開始,保存初始坐標
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        self.panGestureStartPointX = gesturePoint.x;
    }
    //滑動過程中,動態改變 menuView 位置及 backgroundView 透明度
    else if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
        CGFloat deltaX = 0;
        
        //計算手指相對起始位置的滑動距離
        deltaX = gesturePoint.x - self.panGestureStartPointX;
        
        //如果滑動距離是負數,則說明手指滑動方向與側欄回收方向相反,無需處理
        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];
        }
    }
    //滑動結束后,判斷該彈出還是收回 menuView
    else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        CGFloat minOffset = [self.containerViewController getMinOffset];
        //計算 menuView 的最終位移
        CGFloat viewOffset = self.containerViewController.rightMenuContainerView.frame.origin.x;
        //彈出/收回側欄
        if (viewOffset > minOffset) {
            [self.containerViewController dismissRightMenu];
            [self.containerViewController dismissBackgroundView];
        }
        else {
            [self.containerViewController showRightMenu];
            [self.containerViewController showBackgroundView];
        }
    }
}

寫了這么多,終于接近尾聲!

3. 坑!

  • 章節 2.6.3 & 2.6.4,為何要在 MenuViewController 中設計 transparentView 用于「透視」ContainersViewController 的黑色半透明背景,而不直接將 MenuContainerView 的寬度固定為有效內容寬度,而非全屏幕?

    • 假設 MenuViewController 寬度不是全屏幕,但使用了 NavigationController ,在調用 pushViewController 后,新頁面寬度將和 MenuViewController 一致,不能全屏顯示。所以這個地方的實現邏輯需要根據項目實際需求修改。
  • MainViewController 中存在 Scroll View,屏幕邊緣滑入不能觸發側欄打開

    • MainViewController 中調用下述代碼,讓 ContainersViewController 的手勢優先級更高
      [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.containersViewController.screenEdgePanGesture];
  • MainViewControllerLeftMenuViewControllerRightMenuViewController 存在頁面跳轉,在跳轉后必須禁止 ContainersViewControllerUIScreenEdgePanGestureRecognizer ,否則頁面跳轉后仍能通過屏幕邊緣滑入手勢彈出側欄

  • 如需要同時使用 TabBarController ,只需在 MainViewControllerNavigationController 前添加一個 TabBarController 即可

4. 寫在最后

對于雖說側欄只是一個很舊的,甚至不被蘋果提倡的功能,不過通過這次「造」輪子,也算比較深入地了解了 Container View ,獲益匪淺。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,596評論 25 708
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一種新的協議。它實...
    香橙柚子閱讀 24,130評論 8 183
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,259評論 4 61
  • (2015-03-17 14:42:08) 我們總是在等,等有時間,等有了錢 等時機成熟,就一定買大別墅 帶著父母...
    榆樹閱讀 1,681評論 0 4
  • 寫給柯俊院士 一路走好 文||與你相識 您的來和您的去 是在安靜的自然里 縱然您的肩頭 扛著祖國強盛的重任 百年的...
    與你相識_40fa閱讀 441評論 0 2