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 最终效果

iOS — 使用 Container View 实现左右侧栏_第1张图片

Figure 1.2 : iOS 页面结构

Figure 1.3 : iOS DrawerLayoutDemo 实现效果

2. 实现过程

2.1 思路

iOS — 使用 Container View 实现左右侧栏_第2张图片

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.

iOS — 使用 Container View 实现左右侧栏_第3张图片

根据苹果的官方介绍,Container View 主要用于将多个页面的内容整合到一个页面,同时,每个 Container View 均对应一个独立的 View Controller,将每个 Container View 的功能解耦,避免主 View Controller 过于臃肿。这样看来,Container View 用于实现 DrawerLayout 最合适不过。

2.3 代码框架搭建

iOS — 使用 Container View 实现左右侧栏_第4张图片

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

运行效果如下:

iOS — 使用 Container View 实现左右侧栏_第5张图片

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 中。

iOS — 使用 Container View 实现左右侧栏_第6张图片

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
iOS — 使用 Container View 实现左右侧栏_第7张图片

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

你可能感兴趣的:(iOS — 使用 Container View 实现左右侧栏)