UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)

版本记录

版本号 时间
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

在本教程中,您将构建一个滑出式面板导航,这是使用普通UINavigationControllerUITabBarController进行应用程序导航的常用替代方法。 滑出式导航面板允许用户将内容滑到屏幕或滑出屏幕。

打开已有的项目,下面的效果就是要达到的最终效果。

滑出导航面板设计模式允许开发人员为他们的应用添加永久导航,而不会占用宝贵的屏幕空间,因为用户可以随时选择显示导航,同时仍然可以看到他们当前的上下文。

在本教程中,您将采用更少的方法,以便您可以相对轻松地将滑出式导航面板技术应用于您自己的应用程序。

从名为SlideOutNavigation.xcodeprojSlideOutNavigation-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开始实现它。

为了使本教程专注于重要的内容,IBActionsIBOutlets在故事板中为您预先连接。 但是,要实现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
    }
  }
}

此代码几乎与左侧面板的代码完全相同,当然,除了方法和属性名称以及方向的差异之外。 如果您对此有任何疑问,可以随时查看上一节中的说明。

与以前一样,IBActionsIBOutlets已经在故事板中为您连接。 与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.beganUIGestureRecognizerState.changedUIGestureRecognizerState.ended。使用switch相应地处理每个。
  • 3) .began:如果用户开始平移且两个面板都不可见,则根据平移方向显示正确的面板并使阴影可见。
  • 4) .changed:如果用户已经在平移,请按用户平移的距离移动中心视图控制器的视图
  • 5) .ended:当拖动结束时,检查左视图控制器或右视图控制器是否可见。根据哪个可见以及平移了多远,执行动画。

您可以使用这三种状态的组合以及平移手势的位置,速度和方向来移动中心视图,显示和隐藏左视图和右视图。

例如,如果手势方向是右侧,则显示左侧面板。如果方向是左侧,则显示右侧面板。

再次构建并运行应用程序。此时,您应该能够左右滑动中央面板,露出下面的面板。

如果您想通过DIY解决方案尝试预建库,请务必查看SideMenu。 有关此UI控件的起源(以及内存通道)的深入讨论,请查看iOS开发人员和设计师Ken Yarmosh发布的New iOS Design Pattern: Slide-Out Navigation。 他很好地解释了使用这种设计模式的好处并展示了常见用途。

后记

本篇主要讲述了如何创建自己的侧滑式面板导航,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容