版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.04.02 星期二 |
前言
iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
在本教程中,您将构建一个滑出式面板导航,这是使用普通UINavigationController
或UITabBarController
进行应用程序导航的常用替代方法。 滑出式导航面板允许用户将内容滑到屏幕或滑出屏幕。
打开已有的项目,下面的效果就是要达到的最终效果。
滑出导航面板设计模式允许开发人员为他们的应用添加永久导航,而不会占用宝贵的屏幕空间,因为用户可以随时选择显示导航,同时仍然可以看到他们当前的上下文。
在本教程中,您将采用更少的方法,以便您可以相对轻松地将滑出式导航面板技术应用于您自己的应用程序。
从名为SlideOutNavigation.xcodeproj
的SlideOutNavigation-Starter
文件夹中打开项目,并查看它是如何组织的。除了视图控制器之外,还有一个名为Assets.xcassets
的资产目录,其中包含您将在应用程序中使用的所有可爱的小猫和小狗图像。
这是这个应用程序的整体结构:
-
ContainerViewController
是魔术发生的地方!这是视图控制器,可以处理动画和中心视图控制器与左右面板之间的滑动等操作。它负责保存对所有其他必要视图控制器的引用。 -
CenterViewController
是中心面板视图控制器。 -
SidePanelViewController
用作左侧和右侧面板视图控制器。
您可以在Main.storyboard
中找到中央,左侧和右侧视图控制器的视图。所以,随便看看整个项目。
现在您已经熟悉了应用程序的结构,现在是时候从正方形 - 中心面板开始了。
Finding Your Center
第一项业务是将CenterViewController
放在ContainerViewController
中作为子视图控制器。
打开ContainerViewController.swift
。 找到viewDidLoad()
并在其上方添加以下属性:
var centerNavigationController: UINavigationController!
var centerViewController: CenterViewController!
这两个属性将同时包含centerViewController
及其父导航控制器。
注意:这些是隐式解包的选项(由!表示)。 它们必须是可选的,因为它们的值在
init()
完成之前不会被初始化,但它们可以隐式解包,因为你知道它们会在你使用它们时被初始化。 如果不是,那么这是一个程序员错误,你想在测试应用程序时知道它。
在文件的底部,您将看到UIStoryboard
的类扩展,其中包含一些静态方法,可以更方便地从应用程序的故事板中加载特定的视图控制器。 您将利用这些方法填充刚刚创建的属性。
在对super
的调用下面的viewDidLoad()
中添加以下代码:
// 1
centerViewController = UIStoryboard.centerViewController()
// 2
centerViewController.delegate = self
// 3
centerNavigationController = UINavigationController(rootViewController: centerViewController)
view.addSubview(centerNavigationController.view)
addChild(centerNavigationController)
// 4
centerNavigationController.didMove(toParent: self)
不要担心第二行上的编译器错误,你很快就会解决这个问题。
这个简短的方法有一些有趣的东西。这是你正在做的事情:
- 1) 通过从故事板中拉出它来获取
centerViewController
。 - 2) 将当前视图控制器设置为中心视图控制器的委托,以便中心视图控制器可以通知其容器何时显示和隐藏左侧和右侧面板。
- 3) 创建一个导航控制器以包含中心视图控制器,以便您可以将视图推送到它并在导航栏中显示条形按钮项目。然后,将导航控制器的视图添加到
ContainerViewController
的视图中。 - 4) 使用
addChild(_ :)
和didMove(toParent :)
设置父子关系。
真棒!现在要处理下错误,修改此类以便它实现CenterViewControllerDelegate
。
将以下类扩展添加到文件底部的UIStoryboard
扩展下面的ContainerViewController
(这还包括一些您将在稍后填写的空方法):
// MARK: CenterViewController delegate
extension ContainerViewController: CenterViewControllerDelegate {
func toggleLeftPanel() {
}
func toggleRightPanel() {
}
func collapseSidePanels() {
}
}
实现这些方法使此类符合CenterViewControllerDelegate
。
现在是检查进度的好时机。 构建并运行应用程序。 您应该看到类似于以下屏幕的内容:
是的,顶部的那些按钮最终将为您带来小猫和小狗。 创建滑动导航面板有什么更好的理由? 但为了让你更可爱,你必须开始滑动。 首先,向左!
Kittens to the Left of Me…
您已创建了中心面板,但添加左视图控制器需要一组不同的步骤。
要展开左侧面板,用户将点击导航栏中的Kitties
按钮。 打开CenterViewController.swift
开始实现它。
为了使本教程专注于重要的内容,IBActions
和IBOutlets
在故事板中为您预先连接。 但是,要实现DIY滑出式导航面板,您需要了解按钮的配置方式。
请注意,已经有两种IBAction
方法,每种方法对应一个按钮。 找到kittiesTapped(_ :)
并将以下实现添加到其中:
delegate?.toggleLeftPanel()
如前所述,该方法已经连接到Kitties
按钮。 仅当delegate
具有值时,这使用可选链接来调用toggleLeftPanel()
。
您可以在底部看到委托协议的定义。 正如您将看到的,有一些方法叫做toggleLeftPanel()
,toggleRightPanel()
和collapseSidePanels()
。 如果您还记得,当您先设置中心视图控制器实例时,将其委托设置为容器视图控制器。 是时候去实现toggleLeftPanel()
了。
注意:有关委托方法及其实现方式的更多信息,请参阅Apple’s Developer Documentation。
打开ContainerViewController.swift
并在ContainerViewController
的顶部添加一个枚举:
enum SlideOutState {
case bothCollapsed
case leftPanelExpanded
case rightPanelExpanded
}
您将使用它来跟踪侧面板的当前状态,因此您可以判断两个面板是否都不可见,或者左侧或右侧面板中的一个是否可见。
接下来,在现有的centerViewController
属性下添加两个属性:
var currentState: SlideOutState = .bothCollapsed
var leftViewController: SidePanelViewController?
这些将保持侧面板和左侧面板视图控制器本身的当前状态:
您将currentState
初始化为.bothCollapsed
- 也就是说,当应用程序首次加载时,两个侧面板都不可见。 leftViewController
属性是可选的,因为您将在不同时间添加和删除视图控制器,因此它可能并不总是具有值。
接下来,添加toggleLeftPanel()
的实现:
let notAlreadyExpanded = (currentState != .leftPanelExpanded)
if notAlreadyExpanded {
addLeftPanelViewController()
}
animateLeftPanel(shouldExpand: notAlreadyExpanded)
首先,此方法检查左侧面板是否已展开。 如果它尚未显示,则调用一个方法将面板添加到视图层次结构中。 然后,它调用另一种方法,将其动画为open
位置。 如果面板已经可见,则它将面板设置为closed
位置的动画。
接下来,您需要添加代码以将左侧面板添加到视图层次结构中。 在下面添加以下toggleLeftPanel()
:
func addLeftPanelViewController() {
guard leftViewController == nil else { return }
if let vc = UIStoryboard.leftViewController() {
vc.animals = Animal.allCats()
addChildSidePanelController(vc)
leftViewController = vc
}
}
此代码首先检查leftViewController
属性是否为nil
。 如果是,则创建一个新的SidePanelViewController
并设置要显示的动物列表 - 在这种情况下,猫!
接下来,在扩展的底部添加addChildSidePanelController(_ :)
的实现:
func addChildSidePanelController(_ sidePanelController: SidePanelViewController) {
view.insertSubview(sidePanelController.view, at: 0)
addChild(sidePanelController)
sidePanelController.didMove(toParent: self)
}
此方法将子视图控制器添加到容器视图控制器。 这与先前添加中心视图控制器相同。 它只是插入它的视图 - 在这种情况下,它插入z-index 0
,这意味着它将在中心视图控制器下面 - 并将其添加为子视图控制器。
在ContainerViewController
顶部的其他属性下面添加以下常量:
let centerPanelExpandedOffset: CGFloat = 90
此值是中心视图控制器在屏幕上设置动画后可见的宽度(以磅为单位)。 90
个点应该可以了。
接下来,返回CenterViewControllerDelegate
扩展,在addLeftPanelViewController()
下面添加以下内容:
func animateLeftPanel(shouldExpand: Bool) {
if shouldExpand {
currentState = .leftPanelExpanded
animateCenterPanelXPosition(
targetPosition: centerNavigationController.view.frame.width
- centerPanelExpandedOffset)
} else {
animateCenterPanelXPosition(targetPosition: 0) { _ in
self.currentState = .bothCollapsed
self.leftViewController?.view.removeFromSuperview()
self.leftViewController = nil
}
}
}
此方法首先检查是否已告知它是否展开或折叠侧面板。 如果它应该展开,则它设置当前状态以指示左面板已展开并调用方法为中心面板设置动画以使其打开。 否则,它会动画中心面板关闭,移除其视图并设置当前状态以指示它已关闭。
现在在addChildSidePanelController(_ :)
上面添加这个方法:
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)
}
这是实际动画发生的地方。 它使用漂亮的弹簧动画将中心视图控制器的视图移动到指定位置。 该方法还采用可选的完成闭包,并将其传递给UIView动画。 如果要更改动画的外观,可以尝试调整持续时间和弹簧阻尼参数。
构建并运行应用程序。
点击导航栏中的Kitties
。 中央视图控制器应该滑过 - 嗖! - 并显示下面的Kitties
菜单。 噢,看看他们都很可爱。
但太多的可爱可能是一件危险的事情! 再次点击Kitties
按钮隐藏它们!
Me and My Shadow
当左侧面板打开时,请注意它是如何正对着中央视图控制器的。 如果它们之间有一点间隔,那就太好了。 添加阴影怎么样?
仍然在ContainerViewController.swift
中,将以下方法添加到扩展的末尾:
func showShadowForCenterViewController(_ shouldShowShadow: Bool) {
if shouldShowShadow {
centerNavigationController.view.layer.shadowOpacity = 0.8
} else {
centerNavigationController.view.layer.shadowOpacity = 0.0
}
}
这会调整导航控制器阴影的不透明度,使其可见或隐藏。 每当currentState
属性更改时,您将实现didSet
观察器以添加或删除阴影。
滚动到ContainerViewController.swift
的顶部并将currentState
声明更改为:
var currentState: SlideOutState = .bothCollapsed {
didSet {
let shouldShowShadow = currentState != .bothCollapsed
showShadowForCenterViewController(shouldShowShadow)
}
}
只要属性的值发生变化,就会执行didSet
闭包。 如果任一面板可见,则显示阴影。
再次构建并运行应用程序。 这次当你点击Kitties
时,看看甜蜜的新影子! 看起来更好,对吧?
接下来,为右侧添加相同的功能,这意味着...小狗!
Puppies to the Right…
要添加右侧面板视图控制器,只需重复添加左侧视图控制器的步骤。
打开ContainerViewController.swift
并在leftViewController
下面添加以下属性:
var rightViewController: SidePanelViewController?
接下来,找到toggleRightPanel()
并添加以下实现:
let notAlreadyExpanded = (currentState != .rightPanelExpanded)
if notAlreadyExpanded {
addRightPanelViewController()
}
animateRightPanel(shouldExpand: notAlreadyExpanded)
接下来,添加以下toggleRightPanel()
:
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 = .rightPanelExpanded
animateCenterPanelXPosition(
targetPosition: -centerNavigationController.view.frame.width
+ centerPanelExpandedOffset)
} else {
animateCenterPanelXPosition(targetPosition: 0) { _ in
self.currentState = .bothCollapsed
self.rightViewController?.view.removeFromSuperview()
self.rightViewController = nil
}
}
}
此代码几乎与左侧面板的代码完全相同,当然,除了方法和属性名称以及方向的差异之外。 如果您对此有任何疑问,可以随时查看上一节中的说明。
与以前一样,IBActions
和IBOutlets
已经在故事板中为您连接。 与Kitties
类似,Puppies
连接到名为puppiesTapped(_ :)
的IBAction
方法。 此按钮控制中央面板的滑动以显示右侧面板。
切换到CenterViewController.swift
并将以下行添加到puppiesTapped(_ :)
:
delegate?.toggleRightPanel()
同样,这与kittiesTapped(_ :)
相同,只是它切换右侧面板而不是左侧面板。
是时候看一些小狗了!
再次构建并运行应用程序以确保一切正常。 点击小狗。 您的屏幕应如下所示:
看起来不错吧? 但请记住,你不想让自己暴露在幼犬的可爱中太长时间,所以再次点击该按钮以隐藏它们。
您现在可以查看小猫和小狗,但是能够查看每个小猫的大图非常棒,不是吗?
Pick an Animal, Any Animal!
小猫和小狗列在左侧和右侧小组中。 这些都是SidePanelViewController
的实例,它只包含一个table view
。
打开SidePanelViewController.swift
以查看SidePanelViewControllerDelegate
协议。 无论何时点击动物,都可以通过此方法通知侧面板的代表。 是时候用了!
在SidePanelViewController.swift
中,在类顶部添加以下属性,在表视图IBOutlet
下面:
var delegate: SidePanelViewControllerDelegate?
然后,在UITableViewDelegate
扩展中填写tableView(_:didSelectRowAt :)
的实现:
let animal = animals[indexPath.row]
delegate?.didSelectAnimal(animal)
如果有一个委托集,这将告诉用户选择了一个动物。 目前,没有代理! 将CenterViewController
作为侧面板的委托是有意义的,因为它可以显示所选的动物照片和标题。
打开CenterViewController.swift
以实现委托协议。 在现有类定义下添加以下扩展:
extension CenterViewController: SidePanelViewControllerDelegate {
func didSelectAnimal(_ animal: Animal) {
imageView.image = animal.image
titleLabel.text = animal.title
creatorLabel.text = animal.creator
delegate?.collapseSidePanels()
}
}
此方法只使用动物的图像,标题和创建者填充中心视图控制器中的图像视图和标签。 然后,如果中心视图控制器具有自己的委托,则告诉它折叠侧面板,以便您可以专注于所选项目。
但是,collapseSidePanels()
不会做任何事情! 打开,ContainerViewController.swift
并将以下实现添加到collapseSidePanels()
:
switch currentState {
case .rightPanelExpanded:
toggleRightPanel()
case .leftPanelExpanded:
toggleLeftPanel()
case .bothCollapsed:
break
}
此方法中的switch
语句只检查侧面板的当前状态,并折叠打开的任何一个。
现在,找到addChildSidePanelController(_ :)
并将以下内容添加到方法的底部:
sidePanelController.delegate = centerViewController
除了之前的工作之外,该方法现在将中心视图控制器设置为侧面板的委托。
应该这样做!
构建并运行应用程序。 查看小猫或小狗,然后点击其中一个可爱的小动物。 侧面板应该再次折叠,你应该看到你选择的动物的细节。
Move Your Hands Back and Forth
导航栏按钮很棒,但大多数应用程序还允许您滑动以打开侧面板。 向您的应用添加手势非常简单。 不要被暗示,你会做得很好!
再次打开ContainerViewController.swift
。 首先,通过在UIStoryboard
扩展上方添加以下扩展,使此类符合UIGestureRecognizerDelegate
:
// MARK: Gesture recognizer
extension ContainerViewController: UIGestureRecognizerDelegate {
@objc func handlePanGesture(_ recognizer: UIPanGestureRecognizer) {
}
}
接下来,找到viewDidLoad()
。 将以下内容添加到方法的末尾:
let panGestureRecognizer = UIPanGestureRecognizer(
target: self,
action: #selector(handlePanGesture(_:)))
centerNavigationController.view.addGestureRecognizer(panGestureRecognizer)
这将创建一个UIPanGestureRecognizer
,将self
指定为target
,并将handlePanGesture(_ :)
作为选择器来处理任何检测到的拖动手势。
默认情况下,拖动手势识别器会检测单个手指的单次触摸,因此不需要任何额外配置。 您只需要将新创建的手势识别器添加到centerNavigationController
视图中。
我不是告诉你这很简单吗? 您的滑出导航面板例程中只剩下一个移动。 手势识别器在检测到手势时调用handlePanGesture(_ :)
。 因此,本教程的最后一项任务是实现该方法。
将以下代码块添加到handlePanGesture(_ :)
(这是一个很多代码!):
// 1
let gestureIsDraggingFromLeftToRight = (recognizer.velocity(in: view).x > 0)
// 2
switch recognizer.state {
// 3
case .began:
if currentState == .bothCollapsed {
if gestureIsDraggingFromLeftToRight {
addLeftPanelViewController()
} else {
addRightPanelViewController()
}
showShadowForCenterViewController(true)
}
// 4
case .changed:
if let rview = recognizer.view {
rview.center.x = rview.center.x + recognizer.translation(in: view).x
recognizer.setTranslation(CGPoint.zero, in: view)
}
// 5
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 halfway
let hasMovedGreaterThanHalfway = rview.center.x > view.bounds.size.width
animateLeftPanel(shouldExpand: hasMovedGreaterThanHalfway)
} else if let _ = rightViewController,
let rview = recognizer.view {
let hasMovedGreaterThanHalfway = rview.center.x < 0
animateRightPanel(shouldExpand: hasMovedGreaterThanHalfway)
}
default:
break
}
下面进行细分:
- 1) 拖动手势识别器可以检测任何方向的平移,但您只对水平移动感兴趣。首先,设置
gestureIsDraggingFromLeftToRight
布尔值,以使用手势速度的x分量来检查这一点。 - 2) 需要跟踪三种状态:
UIGestureRecognizerState.began
,UIGestureRecognizerState.changed
和UIGestureRecognizerState.ended
。使用switch
相应地处理每个。 - 3)
.began
:如果用户开始平移且两个面板都不可见,则根据平移方向显示正确的面板并使阴影可见。 - 4)
.changed
:如果用户已经在平移,请按用户平移的距离移动中心视图控制器的视图 - 5)
.ended
:当拖动结束时,检查左视图控制器或右视图控制器是否可见。根据哪个可见以及平移了多远,执行动画。
您可以使用这三种状态的组合以及平移手势的位置,速度和方向来移动中心视图,显示和隐藏左视图和右视图。
例如,如果手势方向是右侧,则显示左侧面板。如果方向是左侧,则显示右侧面板。
再次构建并运行应用程序。此时,您应该能够左右滑动中央面板,露出下面的面板。
如果您想通过DIY解决方案尝试预建库,请务必查看SideMenu。 有关此UI控件的起源(以及内存通道)的深入讨论,请查看iOS开发人员和设计师Ken Yarmosh发布的New iOS Design Pattern: Slide-Out Navigation。 他很好地解释了使用这种设计模式的好处并展示了常见用途。
后记
本篇主要讲述了如何创建自己的侧滑式面板导航,感兴趣的给个赞或者关注~~~