iOS — 使用 Container View 实现左右侧栏

2016-11-07  本文已影响592人  ChamchamBen

1. 前言

前段时间做 Android 开发时使用系统控件 DrawerLayout 轻松实现了左右侧栏,最近做 iOS 开发时恰好需要用到,本想着跟 Android 一样会有系统级的控件提供,谁料 Apple 并不提倡使用「侧栏」的交互模式,未提供相关控件。由于项目需要用到左右侧栏,摆在面前的只有两个选择:使用第三方开源库,或自己造轮子。
首先,我们先看看使用 Android 系统控件实现的侧栏效果:

Figure 1.1 : DrawerLayout 实现效果

从图1.1中可以看出,DrawerLayout 大致实现了下述几种交互:

在下载尝试了几款 iOS 侧栏开源库后,发现基本上都不能满足 DrawerLayout 的相同交互,而且大部分都是使用纯代码实现,与公司 iOS 项目遵循的 storyboard 优先原则有所违背,最终决定造个轮子,让 iOS 及 Android app 的 UX/UI 尽可能一致。

1.1 开发环境

1.2 工具

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 ,分别对应左侧栏、主页面和右侧栏,并实现

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

考虑到使用系统内建的 Navigation Bar ,以及 MainViewController 里面通常都会有一些 push navigation 的页面跳转需求,故通过 Editor -> Embed in -> Navigation ControllerMainViewController 增加一个 Navigation Controller 作为 parent controller ,同理,可使用相同方式添加 Tab Bar Controller

2.3.2 代码目录搭建

对应 Main.storyboard 中的页面,新建

做好必要的 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 需要实现的功能

讲到这里,相信大家都可以明显地感受到 Container View 的好处。通过使用 Container ViewViewController 的功能进行解耦,在避免产生单个臃肿 ViewController 的同时,又能很好地实现复杂的单页面功能;同时对多尺寸、横竖屏的适配也更灵活方便,推荐大家多使用。

这里插播一下,上面功能分析提到的「通知」,有很多种实现方式,包括但不限于 NSNotificationCenterDelegate函数调用 。本教程的「通知」使用的是 函数调用 的方式。

「万事俱备,只欠东风」,功能分解完毕,接下来只需逐个击破!

2.5 ContainersViewController

首先,记得将章节 2.3.2 中的测试代码删除。

- (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)];
}
- (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];
    }];
}
#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)];
    }];
}
- (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];
            }
        }
    }
}

使用 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

Figure 2.5 : LeftMenuViewController 页面

- (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];
}

- (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. 坑!

4. 写在最后

对于虽说侧栏只是一个很旧的,甚至不被苹果提倡的功能,不过通过这次「造」轮子,也算比较深入地了解了 Container View ,获益匪浅。

上一篇 下一篇

猜你喜欢

热点阅读