Swift - 侧滑菜单的实现(样例1:主页向右滑动,露出下方菜单页)
作者:hangge | 2016-01-22 08:50
(本文代码已升级至Swift4)
侧滑菜单是现在的APP上很常见的功能,其效果是在主界面用手指向右滑动,就可以将菜单展示出来,而主界面会被隐藏大部分,但是仍有左侧的一小部分同菜单一起展示。
虽然网上也有很多实现这种slide view效果的第三方库,但如果想自己写代码实现也是很简单的,效果图如下:
虽然网上也有很多实现这种slide view效果的第三方库,但如果想自己写代码实现也是很简单的,效果图如下:
一、基本功能实现
1,程序页面结构
MainViewController:主页视图MenuViewController:菜单视图,当主视图侧滑后显示
ViewController:页面容器视图,将上面两个视图加入到这里面
2,StoryBoard 配置
在StoryBoard中添加两个新的View Controller(Storyoard ID分别是mainView、menuView),同时分别绑定MainViewController和MenuViewController这两个类。3,代码讲解
(1)页面初始化完毕后我们会先把主页视图(MainViewController)添加进来,同时对其设置个拖动手势(UIPanGestureRecognizer)。
(2)当手指在屏幕从左向右滑动时,创建菜单视图(MenuViewController)并添加到页面最底部。同时主页视图会随着手指的移动做线性移动。
(3)手指离开后,根据主页视图的位置(是否滑动超过一半),程序自动将主页视图完全展开或收起。
(2)当手指在屏幕从左向右滑动时,创建菜单视图(MenuViewController)并添加到页面最底部。同时主页视图会随着手指的移动做线性移动。
(3)手指离开后,根据主页视图的位置(是否滑动超过一半),程序自动将主页视图完全展开或收起。
(4)菜单完全展开时,手指点击主页突出的部分也会自动收起菜单。
(5)menuViewExpandedOffset属性是设置菜单展示出来后,主页面在左侧露出部分的宽度。
(6)currentState属性保存菜单的状态,同时监听它的didSet事件来设置主页面阴影(当菜单显示出来的时候,主页边框会添加阴影,这样有层次感,效果更好些。)
(5)menuViewExpandedOffset属性是设置菜单展示出来后,主页面在左侧露出部分的宽度。
(6)currentState属性保存菜单的状态,同时监听它的didSet事件来设置主页面阴影(当菜单显示出来的时候,主页边框会添加阴影,这样有层次感,效果更好些。)
4,ViewController.swift 代码如下
import UIKit class ViewController: UIViewController { // 主页面控制器 var mainViewController:MainViewController! // 菜单页控制器 var menuViewController:MenuViewController? // 菜单页当前状态 var currentState = MenuState.Collapsed { didSet { //菜单展开的时候,给主页面边缘添加阴影 let shouldShowShadow = currentState != .Collapsed showShadowForMainViewController(shouldShowShadow: shouldShowShadow) } } // 菜单打开后主页在屏幕右侧露出部分的宽度 let menuViewExpandedOffset: CGFloat = 60 override func viewDidLoad() { super.viewDidLoad() //添加主页面 mainViewController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "mainView") as! MainViewController view.addSubview(mainViewController.view) //建立父子关系 addChildViewController(mainViewController) mainViewController.didMove(toParentViewController: self) //添加拖动手势 let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) mainViewController.view.addGestureRecognizer(panGestureRecognizer) //单击收起菜单手势 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture)) mainViewController.view.addGestureRecognizer(tapGestureRecognizer) } //拖动手势响应 @objc func handlePanGesture(_ recognizer: UIPanGestureRecognizer) { switch(recognizer.state) { // 刚刚开始滑动 case .began: // 判断拖动方向 let dragFromLeftToRight = (recognizer.velocity(in: view).x > 0) // 如果刚刚开始滑动的时候还处于主页面,从左向右滑动加入侧面菜单 if (currentState == .Collapsed && dragFromLeftToRight) { currentState = .Expanding addMenuViewController() } // 如果是正在滑动,则偏移主视图的坐标实现跟随手指位置移动 case .changed: let positionX = recognizer.view!.frame.origin.x + recognizer.translation(in: view).x //页面滑到最左侧的话就不许要继续往左移动 recognizer.view!.frame.origin.x = positionX < 0 ? 0 : positionX recognizer.setTranslation(.zero, in: view) // 如果滑动结束 case .ended: //根据页面滑动是否过半,判断后面是自动展开还是收缩 let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width animateMainView(shouldExpand: hasMovedhanHalfway) default: break } } //单击手势响应 @objc func handleTapGesture() { //如果菜单是展开的点击主页部分则会收起 if currentState == .Expanded { animateMainView(shouldExpand: false) } } // 添加菜单页 func addMenuViewController() { if (menuViewController == nil) { menuViewController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "menuView") as? MenuViewController // 插入当前视图并置顶 view.insertSubview(menuViewController!.view, at: 0) // 建立父子关系 addChildViewController(menuViewController!) menuViewController!.didMove(toParentViewController: self) } } //主页自动展开、收起动画 func animateMainView(shouldExpand: Bool) { // 如果是用来展开 if (shouldExpand) { // 更新当前状态 currentState = .Expanded // 动画 animateMainViewXPosition(targetPosition: mainViewController.view.frame.width - menuViewExpandedOffset) } // 如果是用于隐藏 else { // 动画 animateMainViewXPosition(targetPosition: 0) { finished in // 动画结束之后s更新状态 self.currentState = .Collapsed // 移除左侧视图 self.menuViewController?.view.removeFromSuperview() // 释放内存 self.menuViewController = nil; } } } //主页移动动画(在x轴移动) func animateMainViewXPosition(targetPosition: CGFloat, completion: ((Bool) -> Void)! = nil) { //usingSpringWithDamping:1.0表示没有弹簧震动动画 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { self.mainViewController.view.frame.origin.x = targetPosition }, completion: completion) } //给主页面边缘添加、取消阴影 func showShadowForMainViewController(shouldShowShadow: Bool) { if (shouldShowShadow) { mainViewController.view.layer.shadowOpacity = 0.8 } else { mainViewController.view.layer.shadowOpacity = 0.0 } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } } // 菜单状态枚举 enum MenuState { case Collapsed // 未显示(收起) case Expanding // 展开中 case Expanded // 展开 }源码下载:hangge_1028.zip
二、功能改进
1,改进说明
如果只有左右滑动能调出菜单的话,会显得把菜单功能隐藏太深,可能用户使用半天还不知道有这个菜单。所以通常除了滑动调出菜单,页面上也会提供个菜单按钮,一般放置在导航栏上。点击按钮同样可以打开,收起菜单。效果图如下:
2,实现步骤
(1)在StoryBoard中,点击首页面(Main)的Scene,选择Editor -> Embed In -> Navigation Controller 添加导航控制器(2)设置导航控制器的StoryBoard ID为 mainNavigaiton,同时给主页面的导航栏左侧添加一个菜单按钮。
(3)ViewController.swift 代码如下(高亮处为修改过的地方):
import UIKit class ViewController: UIViewController { // 主页导航控制器 var mainNavigationController:UINavigationController! // 主页面控制器 var mainViewController:MainViewController! // 菜单页控制器 var menuViewController:MenuViewController? // 菜单页当前状态 var currentState = MenuState.Collapsed { didSet { //菜单展开的时候,给主页面边缘添加阴影 let shouldShowShadow = currentState != .Collapsed showShadowForMainViewController(shouldShowShadow: shouldShowShadow) } } // 菜单打开后主页在屏幕右侧露出部分的宽度 let menuViewExpandedOffset: CGFloat = 60 override func viewDidLoad() { super.viewDidLoad() //初始化主视图 mainNavigationController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "mainNavigaiton") as! UINavigationController view.addSubview(mainNavigationController.view) //指定Navigation Bar左侧按钮的事件 mainViewController = mainNavigationController.viewControllers.first as! MainViewController mainViewController.navigationItem.leftBarButtonItem?.action = #selector(showMenu) //添加拖动手势 let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) mainNavigationController.view.addGestureRecognizer(panGestureRecognizer) //单击收起菜单手势 let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture)) mainNavigationController.view.addGestureRecognizer(tapGestureRecognizer) } //导航栏左侧按钮事件响应 @objc func showMenu() { //如果菜单是展开的则会收起,否则就展开 if currentState == .Expanded { animateMainView(shouldExpand: false) }else { addMenuViewController() animateMainView(shouldExpand: true) } } //拖动手势响应 @objc func handlePanGesture(_ recognizer: UIPanGestureRecognizer) { switch(recognizer.state) { // 刚刚开始滑动 case .began: // 判断拖动方向 let dragFromLeftToRight = (recognizer.velocity(in: view).x > 0) // 如果刚刚开始滑动的时候还处于主页面,从左向右滑动加入侧面菜单 if (currentState == .Collapsed && dragFromLeftToRight) { currentState = .Expanding addMenuViewController() } // 如果是正在滑动,则偏移主视图的坐标实现跟随手指位置移动 case .changed: let positionX = recognizer.view!.frame.origin.x + recognizer.translation(in: view).x //页面滑到最左侧的话就不许要继续往左移动 recognizer.view!.frame.origin.x = positionX < 0 ? 0 : positionX recognizer.setTranslation(.zero, in: view) // 如果滑动结束 case .ended: //根据页面滑动是否过半,判断后面是自动展开还是收缩 let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width animateMainView(shouldExpand: hasMovedhanHalfway) default: break } } //单击手势响应 @objc func handleTapGesture() { //如果菜单是展开的点击主页部分则会收起 if currentState == .Expanded { animateMainView(shouldExpand: false) } } // 添加菜单页 func addMenuViewController() { if (menuViewController == nil) { menuViewController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "menuView") as? MenuViewController // 插入当前视图并置顶 view.insertSubview(menuViewController!.view, at: 0) // 建立父子关系 addChildViewController(menuViewController!) menuViewController!.didMove(toParentViewController: self) } } //主页自动展开、收起动画 func animateMainView(shouldExpand: Bool) { // 如果是用来展开 if (shouldExpand) { // 更新当前状态 currentState = .Expanded // 动画 animateMainViewXPosition(targetPosition: mainNavigationController.view.frame.width - menuViewExpandedOffset) } // 如果是用于隐藏 else { // 动画 animateMainViewXPosition(targetPosition: 0) { finished in // 动画结束之后s更新状态 self.currentState = .Collapsed // 移除左侧视图 self.menuViewController?.view.removeFromSuperview() // 释放内存 self.menuViewController = nil; } } } //主页移动动画(在x轴移动) func animateMainViewXPosition(targetPosition: CGFloat, completion: ((Bool) -> Void)! = nil) { //usingSpringWithDamping:1.0表示没有弹簧震动动画 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { self.mainNavigationController.view.frame.origin.x = targetPosition }, completion: completion) } //给主页面边缘添加、取消阴影 func showShadowForMainViewController(shouldShowShadow: Bool) { if (shouldShowShadow) { mainNavigationController.view.layer.shadowOpacity = 0.8 } else { mainNavigationController.view.layer.shadowOpacity = 0.0 } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } } // 菜单状态枚举 enum MenuState { case Collapsed // 未显示(收起) case Expanding // 展开中 case Expanded // 展开 }源码下载:hangge_1028.zip
全部评论(10)
楼主 主页的一个分栏控制器 然后点击其他的下面的UITabBarItem 就没有反应了 是不是收拾冲突了呢
站长回复:这个我也不抬确定,只能你自己调试了。
站长,现在侧滑没有什么太大问题了,就是滑动回来的时候会报错:Presenting view controllers on detached view controllers is discouraged 这个好像是视图层次问题,我正在找方法,您看看有没有什么办法
站长回复:我不太清楚你那边代码是怎么写的,这个要靠你自己调试了。
站长,还是7楼的问题,我改了Show Detail(Replace)方式。依然不行,还是会有系统的导航栏把我的覆盖掉
我的配置和您说的一摸一样,是不是因为我新的controller带着导航控制器的缘故,我这里除了view controller剩下的全带着导航控制器,会不会因为这样导致这个问题呢,我是a->b,a是导航控制器,我从b用sugue跳到viewcontroller出现的导航拦覆盖
如果可以的话,站长最好设置一下让我们可以上传图片,文字解释太麻烦了。。。
站长回复:既然是Replace,那么跳到viewcontroller时,如果viewcontroller里原来没有导航栏就肯定不会出现导航栏的。你这个要调试下程序才能找到原因,暂时帮不了你了。
站长,我有一个疑问
我从别的界面通过sugue跳转到页面容器视图(跳转到主视图不能划出侧栏),它上面的系统导航栏会自动出现back键把我的侧栏健覆盖了,有什么办法解决吗?
站长回复:我试了下,sugue跳转到页面容器视图时使用Show Detail(Replace)方式是不会出现这个问题的。
站长,不要mian(那个蓝色的界面),从根视图直接滑动可以吗,可以的话该怎么做呢,和这个一样吗?因为我要做在一个长界面(竖着滑动)的侧滑菜单,但是用了您的demo发现在main里设置竖直方向的滑动没有用,所以问一下可以直接从一个长界面直接侧滑出菜单吗?
站长回复:你说的可能和文章里这个样例不大一样,你的意思是要菜单滑出吧。而本文是主视图滑出,露出下方菜单。你这个需求只能自己研究实现下了。
航哥,你好,这两个demo我都仔细阅读了,尝试着不用stroyboard去完成效果,但是没能成功。
能否针对 完全不用storyboard这个前提,给出这里 侧滑菜单demo的修改建议?
查阅网上资料,大致意思是, 此处的viewController只是作为根视图容器,mainVC和menuVC是非根视图。
站长回复:其实用不用storyboard原理都一样,都是把viewController作为根视图容器,mainVC和menuVC是非根视图。
如果不想用storyboard,把MainViewController和MenuViewController创建方式修改下即可,比如:
mainViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("mainView") as! MainViewController
改成
mainViewController = MainViewController()
MenuViewController也一样。
附上完整代码:hangge_1028.zip
let hasMovedHalf = gesture.view!.frame.origin.x > kWidth * 0.5 ,我觉得用这句来判断是否过半更容易理解吧?kWidth是屏幕宽度
站长回复:一般情况下,不管使用view左上角坐标来判断,还是使用view中点来判断效果都一样,看个人喜好了。
我在文中使用中点还为了下一篇文章中这个判断能不用修改:Swift - 侧滑菜单的实现(样例2:仿QQ,菜单带缩放效果)。在view右移的过程中,view会不断地缩小,这时如果用左上角坐标就不能判断view是否实际移动了半个屏幕的距离了。
let hasMovedhanHalfway = recognizer.view!.center.x > view.bounds.size.width
何解?
recognizer.view!.center.x 是怎么算的?
view.bounds.size.width是屏幕的宽吗
站长回复:view.bounds.size.width是屏幕的宽。
recognizer.view!.center.x指主页视图mainView中心点的x坐标,当其值超过屏幕的宽度,说明主页拖动距离超过半个屏幕。
学习了,持续关注航哥
站长回复:^_^
谢谢版主,这个是我提的需求,非常感谢!
站长回复:不客气,我后面还会再写一篇相关文章,你可以关注下。