原文:How to Create Your Own Slide-Out Navigation Panel in Swift
作者:Nicholas Sakaimbo
译者:kmyhy

更新说明:本教程由 Nick Sakaimbo 更新为 iOS 11、Xcode 9 和 Swift 4。原文作者是 Tammy Coron。

本文介绍如何编写一个滑出式导航面板,这是用于替代 UINavigationController 和 UITabBarController 的一种主流做法,它允许用户将内容切入/切出屏幕。

滑出式导航面板这样的设计模式允许开发者在不占用宝贵屏幕空间的情况下为 app 增加一种固定的导航方式。用户可以在任何时候显示这个导航,同时不用隐藏当前显示内容。

在本文,你会用一种“越少越好”的方式将滑动式导航面板轻松地应用到你自己的 app 中。

开始

你将在一个漂亮的猫咪/狗狗图片浏览器中编写这个滑动式导航面板。首先,请下载这个开始项目。这是一个 zip 文件,请保存后解压缩。

然后打开项目,看一下项目结构。Assets 文件夹是一些 asset catalogs,全都是 app 中用于显示的猫狗图片。注意有 3 个主要的 view controller。如果你要将这个教程用到自己的项目中,请注意它们:

  • ContainerViewController: 奇迹将在这里发生!它包含了一个左、中、右的视图,负责处理动画和滑动手势。在这个项目中,它是在 AppDelegate.swift 的application(_:didFinishLaunchingWithOptons:) 方法中被创建和添加到 window 的。
  • CenterViewController: 中间面板。你可以用自己的 view controller(确保你复制了按钮的 action) 来替换它。
  • SidePanelViewController: 左、右面板。也可以用你自己的 view controller 进行替换。

这 3 个 view controller 的视图在 Main.storyboard 中定义,请自行查看以便了解 app 的大致模样。

熟悉完项目的结构,来看看中间面板。

找到你的中心

在这节,你将以子控制器的形式将 CenterViewcontroller 放到 ContainerViewcontroller 中。

注意:这里使用了 iOS 5 中的视图控制器容器的概念。如果你不熟悉,请阅读 iOS 5 by Tutorials 第 22 章“UIViewController 容器”。

打开 ContainerViewController.swift。在文件底部,有一个扩展,用于 UIStoryboard。它添加了几个静态方法,用于简化从 storyboard 中加载某个 view controller 的过程。你等会会用到这些方法。

为 ContainerViewController 添加几个属性,用于保存一个 CenterVierController 和一个 UINavigationController 对象:

var centerNavigationController: UINavigationController!
var centerViewController: CenterViewController!

注意:这里使用了隐式解包(注意 !的使用)。它们肯定是可空的,因为当 init() 方法调用时它们还不会被初始化,但它们会进行自动解包,因为当它们被创建时你能够确保它们总是会有值的。

然后,在 viewDidLoad() 的 super 调用之后添加:

centerViewController = UIStoryboard.centerViewController()
centerViewController.delegate = self// wrap the centerViewController in a navigation controller, so we can push views to it
// and display bar button items in the navigation bar
centerNavigationController = UINavigationController(rootViewController: centerViewController)
view.addSubview(centerNavigationController.view)
addChildViewController(centerNavigationController)centerNavigationController.didMove(toParentViewController: self)

上述代码创建了一个新的 CenterViewController 并将它保存到 centerViewController 属性。然后创建了一个 UINavigationController 用于包含这个 center view controller。然后将 navigation controller 的 view 添加到 ContainerViewController 的 view,并调用 addChildViewController(_:) 和 didMove(toParentViewController:) 方法建立二者的父子关系。

同时也将当前 view controller 设置为 center view controller 的委托。 center view controller 会问当前 view controller 何时显示和隐藏左右面板。

如果现在编译,你会看到在设置 delegate 一句处报错。你必须修改这个类,让它实现 CenterViewControllerDelegate 协议。添加一个扩展来实现这个协议。在 UIStoryboard 扩展后添加(有一些空方法,我们后面实现它们):

// MARK: CenterViewController delegateextension ContainerViewController: CenterViewControllerDelegate {func toggleLeftPanel() {}func toggleRightPanel() {}func addLeftPanelViewController() {}func addRightPanelViewController() {}func animateLeftPanel(shouldExpand: Bool) {}func animateRightPanel(shouldExpand: Bool) {}
}

来检验一下成果。Build & run。如果一切正常,你会看到:

顶部按钮最终会打开猫咪的图片或者狗狗的图片。那为什么还要创建一个滑出式导航面板呢?仅仅是为了看起来好看,你必须实现滑动式。首先,从左边开始!

猫咪们到左边来…

你创建了你的中间面板,但添加左边的 View controller 是不同的一个步骤。这个过程稍有点多,请务必耐心。多想想猫咪们吧!

要展开左侧菜单,用户需要点击 Kitties 按钮。因此请打开 CenterViewController.swift。

为了将精力集中在重要的事情上,IBAction 和 IBOutlet 在 storyboard 已经是建好的了。但是,如果要实现你自己的滑出式导航面板,你必须理解这些按钮是如何被设置的。

注意已经有两个 IBAction 方法了,一个方法针对一个按钮。找到 kittiesTapped(_:) 方法,添加代码:

delegate?.toggleLeftPanel?()

前面提过,这个方法连接到了 Kitties 按钮。

用一个可空链使得只有在 delegate 不为空且 toggleLeftPanel 方法已经实现的情况下才会调用 toggleLeftPanel 方法。

你可以在 CenterViewControllerDelegate.swift 中看一下委托协议的定义。你会看到,有两个 optional 方法 toggleLeftPanel() 和 toggleRightPanel()。还记得吧,在你创建 center view controller 实例时,你将它的 delegate 设置为 container view controller。接下来我们就实现 toggleLeftPanel()。

注意:关于委托方法和如何实现它们的内容,请参考苹果的开发文档。

打开 ContainerViewController.swift。首先,声明一个枚举。在类名之下添加:

class ContainerViewController: UIViewController {enum SlideOutState {case bothCollapsedcase leftPanelExpandedcase rightPanelExpanded}// ...

这个枚举用于保存侧面板的当前状态,你可以用它们表示任何一个面板都不可见,或者左右面板中有一个可见。

然后,在 centerViewController 属性下添加两个属性:

var currentState: SlideOutState = .bothCollapsed
var leftViewController: SidePanelViewController?

分别用于保存当前状态,以及左侧的 view controller:

一开始的初始状态默认为 .bothCollapsed——也就是说两个侧面板都不可见。leftViewController 属性是一个可空类型,因为你会在不同的时候添加和移除这个 view controller,因此它有可能有时候是没有值的。

接着,实现 toggleLeftPanel() 委托方法:

let notAlreadyExpanded = (currentState != .leftPanelExpanded)if notAlreadyExpanded {addLeftPanelViewController()
}animateLeftPanel(shouldExpand: notAlreadyExpanded)

首先,这个方法会检查左面板是否已经展开。如果它未显示,就将面板添加到视图树中,并将其动画到’打开’的位置。如果这个面板已经显示,它则将它动画到’关闭’位置。

然后,用下面的代码将左面板添加到视图树中。找到 addLeftPanelViewController() 方法,在里面添加代码:

guard leftViewController == nil else { return }if let vc = UIStoryboard.leftViewController() {vc.animals = Animal.allCats()addChildSidePanelController(vc)leftViewController = vc
}

这段代码首先检查 leftViewController 属性是否为 nil。如果是,创建一个新的 SidePanelViewController,然后设置它的 animals 数组,也就是要显示的数据——猫咪们!

然后在 addLeftPanelViewController() 下面添加 addChildSidePanelController() 方法:

func addChildSidePanelController(_ sidePanelController: SidePanelViewController) {view.insertSubview(sidePanelController.view, at: 0)addChildViewController(sidePanelController)sidePanelController.didMove(toParentViewController: self)
}

这个方法用于向 container view controller 中添加子 view。这个过程和前面添加 cener view controller 是一样的。首先插入它的 view(这里将插入的 z 位置设置为 0,这样它就会位于 center view controller 的下面),然后添加子控制器。

基本上可以运行项目了,但还有一个事情要做:添加动画!它不需要多少时间!

说好的滑滑滑滑动呢?

首先,在 ContainerViewController 中添加一个常量:

let centerPanelExpandedOffset: CGFloat = 60

这是center view controller 滑开后还有多少像素宽度可见,设置为 60。

接着,找到 animateLeftPanel(shouldExpand:) 方法,添加代码:

if shouldExpand {currentState = .leftPanelExpandedanimateCenterPanelXPosition(targetPosition: centerNavigationController.view.frame.width - centerPanelExpandedOffset)} else {animateCenterPanelXPosition(targetPosition: 0) { finished inself.currentState = .bothCollapsedself.leftViewController?.view.removeFromSuperview()self.leftViewController = nil}
}

这个方法简单地判断侧边面板是要展开还是隐藏。如果是展开,设置 currentState 属性为展开,让中间面板移动以成为“打开”状态。相反,它会让中间面板动画到“关闭”位置,并移除视图,将当前状态置为关闭状态。

最后,在 animatedLeftPanel(shouldExpand:) 方法后面添加animateCenterPanelXPosition(targetPosition:completion:) 方法:

func animateCenterPanelXPosition(targetPosition: CGFloat, completion: ((Bool) -> Void)? = nil) {UIView.animate(withDuration: 0.5,delay: 0,usingSpringWithDamping: 0.8,initialSpringVelocity: 0,options: .curveEaseInOut, animations: {self.centerNavigationController.view.frame.origin.x = targetPosition}, completion: completion)}

这是真正放动画代码的地方。中间控制器的 view 被动画到指定位置,带有弹簧动画效果。这个方法也带一个可控的完成闭包,用于传递给 UIView 动画的 animate 方法。如果你想修改动画的效果,可以调整动画时长和弹簧动画的阻尼系数。

好了……是时候来试试看了,可以 Build & run 一下了。动手吧!

运行 app 时,点击 Kitties 按钮。中间控制器会滑动——刷!——然后露出底下的 Kitties 菜单。噢,看起来好棒。

太可爱了,真是受不了!再次点击 Kitties 按钮,又可以隐藏菜单。

我和影子

当左面板打开时,它正好在中间视图控制器上面。如果让两者之间有一个明显区分将是个不错的事情。可以加一个阴影吗?

若日后是 ContainerViewController,添加方法:

func showShadowForCenterViewController(_ shouldShowShadow: Bool) {if shouldShowShadow {centerNavigationController.view.layer.shadowOpacity = 0.8} else {centerNavigationController.view.layer.shadowOpacity = 0.0}
}

通过修改导航控制器的 shadowOpacity 属性来显示和显示阴影。你可以用 currentState 属性的 didSet 属性观察器来添加、删除这个阴影。

扎到 ContentViewController 的 currentState 定义,将它修改为:

var currentState: SlideOutState = .bothCollapsed {didSet {let shouldShowShadow = currentState != .bothCollapsedshowShadowForCenterViewController(shouldShowShadow)}
}

在 didSet 闭包中,当属性值被改变时,调用上个方法。无论哪一个面板被打开,都显示它们的阴影。

Build & run。当你点击 Kitties 按钮,注意可爱的阴影!这样效果更好,不是吗?

接下来,实现同样的功能,但这次是针对右边栏,也就是……狗狗们!

狗狗们到右边来……

添加右面板视图控制器的方法,和添加左面板的视图控制器一模一样。

打开 ContainerViewController.swift,在 leftViewController 下增加一个属性:

var rightViewController: SidePanelViewController?

然后找到 toggleRightPanel() 方法,添加代码:

let notAlreadyExpanded = (currentState != .rightPanelExpanded)if notAlreadyExpanded {addRightPanelViewController()
}
animateRightPanel(shouldExpand: notAlreadyExpanded)

接着,将 addRightPanelViewController() 和 animateRightPanel(shouldExpand:) 方法修改为:

func addRightPanelViewController() {guard rightViewController == nil else { return }if let vc = UIStoryboard.rightViewController() {vc.animals = Animal.allDogs()addChildSidePanelController(vc)rightViewController = vc}
}func animateRightPanel(shouldExpand: Bool) {if shouldExpand {currentState = .rightPanelExpandedanimateCenterPanelXPosition(targetPosition: -centerNavigationController.view.frame.width + centerPanelExpandedOffset)} else {animateCenterPanelXPosition(targetPosition: 0) { _ inself.currentState = .bothCollapsedself.rightViewController?.view.removeFromSuperview()self.rightViewController = nil}}
}

这些代码基本上和之前左面板实现的方法一样,只不过方法名和属性名以及动画方向不同。如果你对此有疑问,请参考上一节的解释。

和之前一样,IBActions 和 IBOultets 已经建立了连接。和 Kitties 按钮一样,Puppies 按钮连接到 IBAction 方法 puppiesTapped(_:)。这个按钮能够将中间的面板滑开显示出右边的面板。

最后,回到 CenterViewController.swift 在 puppiesTapped(_:) 中添加代码:

delegate?.toggleRightPanel?()

这和 kittiesTapped(_:) 还是一样,只不过 toggleLeftPanel 变成了 toggleRightPanel。

来看一下狗狗们吧!

Build & run,点击 Puppies 按钮,你会看到:

开起来不错,是吧?记住不要让你自己在这些可爱的狗狗面前呆得太长哦,现在再次点击按钮关闭它。

你现在既可以查看猫咪也可以查看狗狗了,但能够查看它们的大图岂不更爽?:]

选一个宠物吧

狗狗和猫咪们分别显示在右面板和左面板中,这是两个 SidePanelViewController 实例,实际上就只包含了 table view 而已。

回到 SidePanelViewControllerDelegate.swift 看一眼 SidePanelViewController 的委托方法。当某个宠物被选中时,委托对象的这个方法会被调用。我们可以利用它!

在 SidePanelViewController.swift 中,在 tableView 属性下添加一个可空的 delegate 属性:

var delegate: SidePanelViewControllerDelegate?

然后在 UITableViewDelegate 扩展的 tableView(_:didSelectRowAt:) 方法中:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {let animal = animals[indexPath.row]delegate?.didSelectAnimal(animal)
}

如果 delegate 不为空,就通知 delegate 某个宠物被选中了。但是现在委托对象都还没有呢!可以用 CenterViewController 作为 side panel 的委托,因为可以用来显示所选宠物的图片和标题。

打开 CenterViewController.swift 实现委托协议。添加一个扩展:

extension CenterViewController: SidePanelViewControllerDelegate {func didSelectAnimal(_ animal: Animal) {imageView.image = animal.imagetitleLabel.text = animal.titlecreatorLabel.text = animal.creatordelegate?.collapseSidePanels?()}
}

这个方法简单地在中间视图控制器中渲染了图片和标签。然后,如果中间视图控制器也有一个委托的话,就告诉它收起 side panel,以便你能够看到所选的宠物。

collapseSidePanels()还没实现。打开 ContainerViewController.swift 在 toggleRightPanel() 下加入:

func collapseSidePanels() {switch currentState {case .rightPanelExpanded:toggleRightPanel()case .leftPanelExpanded:toggleLeftPanel()default:break}
}

该 switch 语句判断当前状态,如果发现有打开着的 side panel 就关闭它。

最后,将 addChildSidePanelViewController(_:) 修改为:

func addChildSidePanelController(_ sidePanelController: SidePanelViewController) {sidePanelController.delegate = centerViewControllerview.insertSubview(sidePanelController.view, at: 0)addChildViewController(sidePanelController)sidePanelController.didMove(toParentViewController: self)
}

除了之前的代码,还将 center view controller 设置为 side panel 的 delegate。

就是这样了!Build & run。查看 Kitties 或者 Puppies,点击其中的某个萌宠。side panel 会收起,你将看到这只宠物的详情。

左右切换

除了导航栏按钮之外,有许多 app 都支持以“滑动”的方式打开 side panel。添加一个手势识别器是很简单的。别怕,你会搞定它!

打开 ContainerViewController.swift 找到 viewDidLoad()。添加:

let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
centerNavigationController.view.addGestureRecognizer(panGestureRecognizer)

这段代码声明了一个 UIPanGestureRecognizer 并指定 handlePanGesture(_:) 为它的平移手势处理器。(等会你再来实现这个方法)

默认,平移手势识别器会监听单指触摸,因此不需要做其它设置了。你只需要将它添加到 centerNavigationController 的 view 中就可以了。

注意:关于 iOS 中的手势识别器的更多内容,可以阅读我们的 UIGestureRecognizer Swift 教程。

在文件底部,UIStoryboard 扩展之上增加一个扩展,让这个类实现 UIGestureRecognizerDelegate 协议。

// MARK: Gesture recognizerextension ContainerViewController: UIGestureRecognizerDelegate {@objc func handlePanGesture(_ recognizer: UIPanGestureRecognizer) {}
}

我说很简单吧!只剩一个步骤就完成了滑动切换面板的功能了。

让视图动起来!

当手势识别器检测到一个手势时,会调用 handlePanGesture(_:) 方法。所以本教程的最后一个任务就是实现这个方法。
Now Move That View!

在上面新增加的这个方法中添加代码:

let gestureIsDraggingFromLeftToRight = (recognizer.velocity(in: view).x > 0)switch recognizer.state {case .began:if currentState == .bothCollapsed {if gestureIsDraggingFromLeftToRight {addLeftPanelViewController()} else {addRightPanelViewController()}showShadowForCenterViewController(true)}case .changed:if let rview = recognizer.view {rview.center.x = rview.center.x + recognizer.translation(in: view).xrecognizer.setTranslation(CGPoint.zero, in: view)}case .ended:if let _ = leftViewController,let rview = recognizer.view {// animate the side panel open or closed based on whether the view// has moved more or less than halfwaylet hasMovedGreaterThanHalfway = rview.center.x > view.bounds.size.widthanimateLeftPanel(shouldExpand: hasMovedGreaterThanHalfway)} else if let _ = rightViewController,let rview = recognizer.view {let hasMovedGreaterThanHalfway = rview.center.x < 0animateRightPanel(shouldExpand: hasMovedGreaterThanHalfway)}default:break
}

平移手势可以检测任意方向的滑动手势,但我们只对水平方向感兴趣。首先,用一个布尔变量 gestureIsDraggingFromLeftToRight 检查这个手势 x 方向上的速度。

我们需要监听这 3 个状态:UIGestureRecognizerState.began、UIGestureRecognizerState.changed 和UIGestureRecognizerState.ended:

  • .began: 当用户开始移动手指,同时两个面板都不可见,则根据移动的方向显示对应的面板和阴影。
  • .changed: 当用户正在平移时,让中心视图跟随用户的手指一起移动。
  • .ended: 当用户平移结束,判断左右控制器是否可见。根据是左还是右以及平移手势划过的距离,执行这个动画。

你可以在中视图上滑动,打开/隐藏左右视图,组合运用平移手势的这三种状态:位置、速度、方向。

例如,如果手势的方向是向右移动,会显示左侧面板。如果方向是向左的,显示右侧面板。

Build& run。你可以在中视图上左右滑动,打开隐藏在下面的左右面板。如果一切顺当……那就好了。

接下来做什么?

恭喜!看完本教程,你就是一个滑出式导航面板的专家了!

希望你喜欢本教程。请在这里下载完成后的项目。我肯定你会喜欢上这些猫咪和狗狗的。

如果你想尝试一些现成的库而不是自己动手,请查看 SideMenu。要深入探讨这个 UI 控件的源代码(或者来一次记忆中的旅行),请看 iOS 开发者和设计者 Ken Yarmosh 的文章“iOS 的新设计模式:滑出式导航”。他对这种模式的好处进行了很好的阐述,并演示了在真实环境下的常见用法。

请在论坛中发表你对滑出式导航的看法。

如何用 Swift 编写滑出式导航面板相关推荐

  1. Swift封装 滑出式导航栏

    前言: 本文将会创建以下几个主类: DWContainerViewController:这包含了左视图,中视图和右视图控制器的视图,并处理动画和滑动等操作. DWCenterViewControlle ...

  2. Android开发笔记(一百零一)滑出式菜单

    可移动页面MoveActivity 滑出式菜单从界面上看,像极了一个水平滚动视图HorizontalScrollView,当然也可以使用HorizontalScrollView来实现侧滑菜单.不过今天 ...

  3. 仿人人客户端向右滑出式菜单

    人人客户端向右滑出式菜单: 试着实现了一个,先上效果图: 下面简单说明一下实现原理: 有两个activity,MainActivity和SettingActivity,实现这个效果两个步骤: 1.点击 ...

  4. 弹出式导航html,基于JS代码实现导航条弹出式悬浮菜单

    1.概述 采用弹出式悬浮菜单,不但可以使网站的导航内容更加清晰,而且不影响页面的整体效果.运行本实例,如图1所示,当鼠标移动到一级导航菜单的标题上时,将弹出悬浮菜单显示该菜单对应的子菜单,鼠标移出时, ...

  5. css鼠标滑过图标显示_CSS和jQuery教程:苹果风格的花式图标滑出导航

    css鼠标滑过图标显示 View demo 查看演示Download Source 下载源 Today I want to show you, how to create an Apple-style ...

  6. 淘宝首页之导航条——弹出式悬浮菜单

    昨天学习了布局,今天要来实现弹出式导航条.布局选的flex布局. 关于弹出式悬浮菜单总结了下大概是以下几步: 1.鼠标放到一级菜单上时二级菜单显示,鼠标移开二级菜单隐藏. 2.打算为二级菜单设置一个d ...

  7. word修订显示修订人_美丽的滑出导航修订

    word修订显示修订人 View demo 查看演示 Download Source 下载源 After I got a lot of feedback for the Beautiful Slide ...

  8. 在 jQuery 中使用滑入滑出动画效果,实现二级下拉导航菜单的显示与隐藏效果

    查看本章节 查看作业目录 需求说明: 在 jQuery 中使用滑入滑出动画效果,实现二级下拉导航菜单的显示与隐藏效果 用户将光标移动到"最新动态页"或"帮助查询" ...

  9. 15款帮助你实现响应式导航的 jQuery 插件

    对于我们大多数人来说,建立一个负责任的布局中最困难的方面是规划和导航的实现.由于没有真正经得起考验的通用解决方案,您可以使用的菜单设计风格将取决于正在建设的网站类型. 无论你正在建设什么类型的网站,在 ...

最新文章

  1. ES批量索引写入时的ID自动生成算法
  2. C#跑马灯,图片滚动,后台获取图片地址。动态绑定图片,imag显示文字
  3. 常见排序算法时间复杂度
  4. WPF-13:资源文件需要手动引用问题
  5. 微信门店小程序怎样创建 门店小程序创建方法简介
  6. Sophos将AI技术用于预防恶意IP的安全解决方案中
  7. auto和decltype的用法总结
  8. Hibernate原生SQL查询
  9. 【Android UI设计与开发】10:滑动菜单栏(二)SlidingMenu 动画效果的实现
  10. SpringBoot 整合 liquibase
  11. linux中SPI相关API函数,linux spi驱动开发学习(一)-----spi子系统架构
  12. 大华监控相机RTSP视频流
  13. linux复制文件到另一台服务器
  14. 微信语音技术原理_语音控制智能家居系统的实现过程和技术详解
  15. 肯德尔系数怎么分析_肯德尔和谐系数
  16. 移植Python3到TQ2440(一)
  17. MySQL总结(十一)子查询-详解
  18. 蓝桥杯训练1:质数判断,同余问题
  19. C语言编程练习 念数字
  20. 已知IP地址和子网掩码求出网络地址、广播地址、地址范围和主机数

热门文章

  1. 2022年全国职业技能大赛网络安全竞赛试题B模块自己解析思路(5)
  2. openzeppelin
  3. java web中的中文乱码问题和解决方法
  4. 物联网下的RFID门禁,图书防盗新变革
  5. JS数组操作 速查手册
  6. 软阴影(PCF、PCSS)
  7. unity打包的安卓无法解析_我是UNITY3D 打包成APK ,安装到手机上就直接报无法解析程序包。...
  8. Java连接数据库报错(类型不匹配,一步解决)
  9. python3 Requests+Sqlite+Pyquery断点下载小说爬虫
  10. 企业在知乎上做问答推广的技巧分析,企业知乎推广营销方法步骤