UI学习: 抽屉视图详解

文章目录

抽屉视图详解

笔者在仿写网易云音乐的时候,需要实现一个抽屉视图的效果,在最开始的时候的思路就是在原来的页面上添加一个新的视图, 设置为隐藏,在点击菜单的时候,让抽屉视图的hidden = NO;

但是在这样尝试之后, 发现这样并不能达到预期的效果,于是,笔者在通过查询资料之后, 了解到了抽屉视图的实现步骤,下面将抽屉视图这一知识点做一整理.

先展示一下我抽屉视图的实现效果:

在一开始,我们会尝试用 presentViewControllerpush 去推抽屉,这是错误的。抽屉视图不是一个独立的页面,而是主视图的"附属子视图".

抽屉视图是直接添加到主窗口或主控制器的 View 上的。它和主内容视图是兄弟关系,而不是父子关系。

抽屉视图的三层架构

要实现完美的抽屉,你需要构建三层结构:

  1. 底层(主内容层):即 self.view
  2. 中层(遮罩层):一个全屏的 UIView,背景色为偏黑色的半透明颜色,初始状态 hidden = YESalpha = 0, 要实当我们点击菜单按钮弹出抽屉视图的时候, 要遮住原先的视图
  3. 顶层(抽屉层):即菜单视图,初始 frame 在屏幕左侧之外

这三个层级的关系为:

遮罩层在主内容层上面, 抽屉层在遮罩层上面

之前说过 ,抽屉视图是直接添加到主窗口或主控制器的 View 上的。它和主内容视图是兄弟关系,而不是父子关系, 我们要创建一个新的视图控制器,称为容器控制器, 将我们原先的 UITabBarController 和 抽屉视图控制器全部包进去

首先我们创建容器视图控制器 DrowerController

objc 复制代码
// DrawerController.h 

@interface DrawerController : UIViewController
@property (nonatomic, strong) UIViewController *mainViewController;
@property (nonatomic, strong) UIViewController *menuViewController;
@property (nonatomic, strong) UIView* maskView;

@property (nonatomic, assign) BOOL isMenuOpen;
@property (nonatomic, assign) CGFloat menuWidth;

- (instancetype)initWithMainViewController:(UIViewController *)mainViewController
                       menuViewController:(UIViewController *)menuViewController;
- (void)openMenu;
- (void)closeMenu;
- (void)switchOpen; 
@end

实现初始化方法, 添加主内容层和抽屉层

objc 复制代码
// DrawerController.m

- (instancetype)initWithMainViewController:(UIViewController *)main
                       menuViewController:(UIViewController *)menu {
    self = [super init];
    if (self) {
        _mainViewController = main;
        _menuViewController = menu;
        _isMenuOpen = NO;
    }
    return self;

再将这个容器控制器设置为根视图控制器, 这个步骤在SceneDelegate.m 文件中完成的

objc 复制代码
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
    UIWindowScene* myscene = (UIWindowScene*) scene;
    self.window = [[UIWindow alloc] initWithWindowScene: myscene];
    UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController: [[ViewController alloc] init]];
    
    HomeController* homeController = [[HomeController alloc] init];
    UINavigationController* homeNav = [[UINavigationController alloc] initWithRootViewController: homeController];
    homeController.tabBarItem = [[UITabBarItem alloc] initWithTitle: @"首页" image: [UIImage systemImageNamed: @"house"] selectedImage: [UIImage systemImageNamed: @"house.fill"]];
    
    SearchController* searchController = [[SearchController alloc] init];
    UINavigationController* searchNav = [[UINavigationController alloc] initWithRootViewController: searchController];
    searchController.tabBarItem =  [[UITabBarItem alloc] initWithTitle: @"搜索" image: [UIImage systemImageNamed: @"magnifyingglass"] selectedImage: [UIImage systemImageNamed: @"magnifyingglass"]];
    
    NoteController* noteController = [[NoteController alloc] init];
    UINavigationController* noteNav = [[UINavigationController alloc] initWithRootViewController: noteController];
    noteController.tabBarItem =  [[UITabBarItem alloc] initWithTitle: @"文章分类" image: [UIImage systemImageNamed: @"square.and.pencil"] selectedImage: [UIImage systemImageNamed: @"square.and.pencil.fill"]];
    
    MyController* myController = [[MyController alloc] init];
    UINavigationController* myNav = [[UINavigationController alloc] initWithRootViewController: myController];
    myController.tabBarItem =  [[UITabBarItem alloc] initWithTitle: @"我的" image: [UIImage systemImageNamed: @"person"] selectedImage: [UIImage systemImageNamed: @"person.fill"]];
    
    UITabBarController* tabBarController = [[UITabBarController alloc] init];
    tabBarController.viewControllers = @[homeNav, searchNav, noteNav, myNav];
    
    MenuViewController* menuViewController = [[MenuViewController alloc] init]; 
    DrawerController* drawerController = [[DrawerController alloc] initWithMainViewController: tabBarController menuViewController: menuViewController];
    self.window.rootViewController = drawerController;
    [self.window makeKeyAndVisible];
}

这里和愿来一样, 就是把TabBarController 添加到了一个新的容器视图控制器, 然后更换了根视图控制器

在DrawerController 的 viewDidLoad 方法中, 我们实现视图层级关系的添加

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];

    // 添加主视图
    [self addChildViewController: self.mainViewController];
    [self.view addSubview: self.mainViewController.view];
    [self.mainViewController didMoveToParentViewController:self];
    [self.mainViewController.view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
    
    // 添加遮罩层
    [self setupMask];
    
    self.menuWidth = self.view.bounds.size.width * 0.7;
    NSLog(@"%lf", self.menuWidth);
    // 添加菜单视图
    [self addChildViewController: self.menuViewController];
    [self.view addSubview: self.menuViewController.view];
    [self.menuViewController didMoveToParentViewController: self];
    [self.menuViewController.view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(self.view).offset(-self.menuWidth);
        make.top.mas_equalTo(self.view);
        make.width.mas_equalTo(self.menuWidth);
        make.bottom.mas_equalTo(self.view);
    }];
//    self.menuViewController.view.frame = CGRectMake(-self.menuWidth, 0, self.menuWidth, self.view.bounds.size.height);
    [self setUpGesture];
}

这里的顺序不可以改变, 要实现 遮罩层在主内容层上面, 抽屉层在遮罩层上面, 就要按照 主内容层, 遮罩层, 抽屉层 的顺序添加

objc 复制代码
[self addChildViewController: self.mainViewController];
    [self.view addSubview: self.mainViewController.view];
    [self.mainViewController didMoveToParentViewController:self];
    [self.mainViewController.view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];

这段代码是 iOS 中视图控制器容的标准实现模板。将 self.mainViewController 作为子控制器 ,添加到当前的 self(父控制器)中。

下面来看遮罩层写法

objc 复制代码
- (void)setupMask {
    self.maskView = [[UIView alloc] init];
    self.maskView.backgroundColor = [UIColor blackColor];
    self.maskView.alpha = 0;
    
    self.maskView.userInteractionEnabled = YES;

    [self.view insertSubview:self.maskView belowSubview:self.menuViewController.view];
    [self.maskView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}

在开始时(抽屉视图没有显示的时候), 遮罩层显示为透明, 在抽屉弹出的时候, 再设置遮罩层半透明

外部交互

这里控制抽屉视图大打开和关闭我是在定义部分声明了一个属性来记录抽屉视图的状态, 在.h文件声明了这样一个方法

objc 复制代码
- (void)switchOpen {
    self.isMenuOpen ? [self closeMenu] : [self openMenu];
}

然后实现抽屉视图的打开和关闭方法

打开抽屉视图

objc 复制代码
- (void)openMenu {
    [self.menuViewController.view mas_updateConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(0);
    }];
    self.maskView.userInteractionEnabled = YES;
    [UIView animateWithDuration:0.3 animations:^{
            self.maskView.alpha = 0.5;
//        self.menuViewController.view.frame = CGRectMake(0, 0, self.menuWidth, self.view.bounds.size.height);

            [self.view layoutIfNeeded];
        } completion:^(BOOL finished) {
            self.isMenuOpen = YES;
        }];
}

关闭抽屉视图

objc 复制代码
- (void)closeMenu {
    [self.menuViewController.view mas_updateConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(-_menuWidth);
    }];
    self.maskView.userInteractionEnabled = NO;
    [UIView animateWithDuration:0.3 animations:^{
            self.maskView.alpha = 0;
//        self.menuViewController.view.frame = CGRectMake(-self.menuWidth, 0, self.menuWidth, self.view.bounds.size.height);

            [self.view layoutIfNeeded];
        } completion:^(BOOL finished) {
            self.isMenuOpen = NO;
        }];
}

然后,在主内容视图, 我添加了菜单按钮,当点击按钮时候, 就通知容器视图控制器调用 swichOpen 方法, 根据抽屉视图的状态打开或关闭抽屉视图

objc 复制代码
// 添加菜单按钮
- (void) setUpNavigation {
    UIBarButtonItem* menus = [[UIBarButtonItem alloc] initWithImage: [UIImage systemImageNamed: @"text.justify"] style: UIBarButtonItemStylePlain target: self action: @selector(pressMenuButton)];
    self.navigationItem.leftBarButtonItem = menus;
}

- (void)pressMenuButton {
    NSLog(@"点击了菜单"); 
    UIViewController *root = self.view.window.rootViewController;
    if ([root isKindOfClass:[DrawerController class]]) {
        [(DrawerController *)root switchOpen];
    }
}