1

作者:Simon NG,时间:2016/7/29
翻译:BNCoding, 如有错误欢迎指出。原文链接

导航是用户交互的一个重要组成部分,有很多种方式提供一个菜单栏让用户来自由切换想访问的功能。在之前的教程中我们介绍了其中的一种就是侧滑栏。下拉菜单则是另一种常用的菜单设计。当用户点击菜单按键的是时候主屏幕中下拉显示出菜单选项。如果你不知道下拉菜单是如何实现的话,不需要忧虑。继续阅读文章马上你就能看见一个演示动画。

在展示下拉菜单的实现之前,这篇文章已经假设你对自定义视图切换有一定的了解。如果你对这个视图切换有关的内容不太熟悉的话,那么你可以先去看下Joyce写的这篇文章

好了现在进入正题。

下拉菜单功能的开始演示

在这篇教程中我们会用Swift语言来实现下拉菜单,下面就是最终效果的一个快速展示:

工程模版

和以往一样,我不希望你从头开始,你可以先去下载起始工程。该起始工程里面包含了storyboard和一些view controller的类。你可以找到两个tableview,其中一个用于主屏幕(嵌在导航控制器中),另一个用于导航菜单。如果你运行程序的话,主页会展示一些虚拟的数据。

再继续下一步之前,我们先花点时间去浏览一些工程熟悉一下代码。

模态展示菜单视图

首先,我们打开Main.storyboard文件,找到里面的两个tableview,因为两者还没有进行segue链接。为了让用户点击menu按键的时候能够展示出菜单,我们按住control键点击menu按键拖动到菜单的tableview。松开按键,在action segue里面选择”present modally“。

如果你现在运行app的话,菜单界面会以模态的形式展现出来。为了退出下拉菜单视图,我们添加一个unwind segue

打开NewsTableViewController.swift文件,然后添加一个unwind action方法:

@IBAction func unwindToHome(segue: UIStoryboardSegue) {
    let sourceController = segue.sourceViewController as! MenuTableViewController
    self.title = sourceController.currentItem
}

接下来,我们回到storyboard,按住control键链接Menutableview中的prototype cellexit图标,在selection segue选项下面选择unwindToHome

如果用户现在点击任意一个菜单的话,那么当前视图会消失而主视图将会呈现出来。通过unwindToHome函数,主视图控制器(即NewsTableViewController)会根据用户选择的菜单相应的改变其标题。为了简单其间我们除了标题我们不会修改主视图中的内容。

除此之外,我们还将设置当前的选中项为白色。在实现这些希望的结果之前我们还要实现一系列的函数方法。

将下面的函数插入到MenuTableViewController类里面:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    let menuTableViewController = segue.sourceViewController as! MenuTableViewController
    if let selectedRow = menuTableViewController.tableView.indexPathForSelectedRow()?.row {
        currentItem = menuItems[selectedRow]
    }
}

在新版本的Swift中上面的代码编译无法通过,后面我会将附上自己修改后的完整代码

这个函数里面我们只是正确设置了当前选择的菜单。

NewsTableViewController.swift文件里面插入下面这个函数,该函数会将当前的标题传递给menu controller。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    let menuTableViewController = segue.destinationViewController as! MenuTableViewController
    menuTableViewController.currentItem = self.title!
}

现在编译并运行程序的话,当你点击菜单按键的时候菜单选择视图会模态的出现,当你选择其中的一个菜单项的时候程序会回到主视图并且相应的修改主视图标题。

创建带动画过渡的下了菜单

当前的菜单视图示使用系统标准的动画进行转场的,现在我们需要创建一个自定义的转场动画。正如我在之前的文章中介绍的那样,自定义视图控制器的转场动画的核心是动画对象,该对象同时遵守UIViewControllerAnimatedTransitioningUIViewControllerTransitioningDelegate协议。我们将会在类中使用这两者来实现动画转场,但是在此之前我们我们先来看看下拉菜单是怎么工作的。当用户点击菜单按键的时候,主视图会慢慢的往下面移动直到它到达指定的位置,也就是视图底部在往下180个单位。

创建下拉菜单动画

为了实现下拉菜单的动画效果,我们会创建一个名为MenuTransitionManager动画管理器。在工程导航中右击创建一个新文件。文件中的创建一个NSObject的子类MenuTransitionManager

类的代码如下:

class MenuTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
    var duration = 0.5
    var isPresenting = false
    
    var snapshot:UIView?
    
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return duration
    }
    
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        // Get reference to our fromView, toView and the container view
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
        
        // Set up the transform for sliding
        let container = transitionContext.containerView()
        let moveDown = CGAffineTransformMakeTranslation(0, container.frame.height - 150)
        let moveUp = CGAffineTransformMakeTranslation(0, -50)
        
        // Add both views to the container view
        if isPresenting {
            toView.transform = moveUp
            snapshot = fromView.snapshotViewAfterScreenUpdates(true)
            container.addSubview(toView)
            container.addSubview(snapshot!)
        }
        
        // Perform the animation
        UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.3, options: nil, animations: {
            
            if self.isPresenting {
                self.snapshot?.transform = moveDown
                toView.transform = CGAffineTransformIdentity
            } else {
                self.snapshot?.transform = CGAffineTransformIdentity
                fromView.transform = moveUp
            }
            
            
            }, completion: { finished in
                
                transitionContext.completeTransition(true)
                if !self.isPresenting {
                    self.snapshot?.removeFromSuperview()
                }
        })
        
    }
    
    
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        isPresenting = false
        return self
    }
    
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        isPresenting = true
        return self
    }
 
}

该类同时遵循了UIViewControllerAnimatedTransitioningUIViewControllerTransitioningDelegate协议。因为以前的文章中已经介绍了,这里我就不会讲解其中的细节了。我们仔细查看其中的animation block(例如animateTransition方法)。

参考之前显示过程,在整个转场动画其间main viewfromViewmenu viewtoView

为了实现我们要的效果,我们配置了两个转场动画。第一个是向下移动main view,另一个则是上移menu view这样当该视图回到原先的位置的时候可以再次响应事件并进行动画转场。稍后运行程序的时候就明白我的意思了。

从iOS7开始,你可以通过UIView-Snapshotting API快速、简单的创建一个轻量级的视图快照。

snapshot = fromView.snapshotViewAfterScreenUpdates(true)

通过调用snapshotViewAfterScreenUpdates函数,你就获得的main view的快照了。有了快照之后,我们就可以在动画转场的容器里添加该快照了。该快照在menu view之后添加以保证该快照在容器的最顶部。

菜单视图的出场动画的实现其实很简单。我们仅仅在主视图的snapshot上面使用moveDown转场将菜单栏视图设置为默认位置。

self.snapshot?.transform = moveDown
toView.transform = CGAffineTransformIdentity

当菜单栏消失的时候采取的动作是相反的。主视图向上滑动并且恢复到默认的位置。另外我们会将快照移出这样真正的主视图就会展现出来。

现在我们打开NewsTableViewController.swift文件并声明一个MenuTransitionManager对象:

var menuTransitionManager = MenuTransitionManager()

prepareForSegue方法里面添加一行代码设置好该动画委托对象。

现在编译运行代码,点击菜单按键你就能看见一个下拉的菜单栏视图了。

检查用户的手势

到现在为止我们只能通过选择某一个菜单选项来使菜单栏视图消失。从用户的角度来考虑的话,其实点击主视图的快照该菜单栏视图也应该消失。但是主视图的快照是没有进行响应的。

事实上snapshot也是一个UIView对象,所以我们可以创建一个UITapGestureRecognizer对象并且将其添加到snapshot上。

当我们对UITapGestureRecognizer对象进行初始化的时候,我们需要传递目标对象和需要被调用的函数名。很明显你可以硬编码一个特定的对象来让菜单栏视图消失,但是为了我们的设计更加的灵活我们声明一个协议对象,并且让委托对象实现该协议中的方法。

MenuTransitionManager.swift里面,我们如下声明该协议:

@objc protocol MenuTransitionManagerDelegate {
    func dismiss()
}

在这里我们定义了一个MenuTransitionManagerDelegate协议,该协议里面还有一个必须要实现的方法。委托对象必须实现dismiss()方法来完成逻辑上的视图消失操作。

MenuTransitionManager类里面,声明一个委托变量:

var delegate:MenuTransitionManagerDelegate?

然后需要处理该点击事件的对象将会被设置为委托对象。

最后,我们需要创建一个UITapGestureRecognizer对象并且添加多snapshot中。一个好的解决方法就是在snapshot变量中使用didset方法。我们将snapshot定义声明进行修改:

var snapshot:UIView? {
    didSet {
        if let _delegate = delegate {
            let tapGestureRecognizer = UITapGestureRecognizer(target: _delegate, action: "dismiss")
            snapshot?.addGestureRecognizer(tapGestureRecognizer)
        }
    }
}

属性观察器(Property observer)是Swift中一个非常强大的特性。在设置属性的值的时候观察器(willSet/didSet)都将会被调用。该特性让我们在变量赋值的前后都可以很方便的立刻采取一些特定的操作。willSet方法会在属性值赋值设置之前立刻被调用,而didSet方法会在属性值赋值设置完成后立刻被调用。

在上面的代码中,我们在属性观察器方法中创建了一个UITapGestureRecognizer对象并且将它添加给了snapshot。所以,每次我们对snapshot变量进行赋值之后都会立马配置一个UITapGestureRecognizer对象。

大部分的工作都差不多完成了。现在我们回到NewsTableViewController.swift中设置该类遵循MenuTransitionManagerDelegate协议并且实现里面的方法。

首先,我们如下修改相关的声明:

class NewsTableViewController: UITableViewController, MenuTransitionManagerDelegate

接下来我们实现其中的方法:

func dismiss() {
    dismissViewControllerAnimated(true, completion: nil)
}

在上面的代码中,我们使用dismissViewControllerAnimated方法来将退出当前的视图。

最后,我们在NewsTableViewController类的prepareForSegue方法中添加一行代码来设置委托对象:

self.menuTransitionManager.delegate = self

任务完成,现在你编译运行程序并点击主视图的snapshot的话,菜单栏视图将会消失。

通过自定义的视图转换动画,你可以大大的提高用户的体验让你的应用脱颖而出。下拉菜单栏紧急是一个示例而已,在你自己的下一个应用中你可以做出自己的动画实现。

为了读者进行参照,你可以下载完整的代码

因为教程的编写的时间较早以及Swift版本的更迭,文章的部分代码已经不在适用或者部分代码有更好的方法来实现。附上我的代码


BigNerdCoding
1.2k 声望125 粉丝

个人寄语: