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 ,获益匪浅。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一种新的协议。它实...
    香橙柚子阅读 22,959评论 8 183
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,616评论 4 59
  • (2015-03-17 14:42:08) 我们总是在等,等有时间,等有了钱 等时机成熟,就一定买大别墅 带着父母...
    榆树阅读 1,619评论 0 4
  • 写给柯俊院士 一路走好 文||与你相识 您的来和您的去 是在安静的自然里 纵然您的肩头 扛着祖国强盛的重任 百年的...
    与你相识_40fa阅读 418评论 0 2