下拉手势关闭 Modal 视图

6,575 阅读10分钟
原文链接: www.jiarui-blog.com

从iOS7开始,用户可以通过从屏幕左侧滑动的手势返回上级目录。这种滑动返回是交互式的,所以动画会跟随你的手势。这个功能对于大屏设备尤其方便,因为返回按钮往往离得太远。

而对于Modal窗口,通常它是从屏幕下方移进屏幕的。开发者们通常会在屏幕右上方添加一个“x”图标用于退出Modal视图。如果我们能通过向下滑动的手势退出岂不更妙?其实已经有不少App是这么设计的了,比如Twitter里的Moments界面。

在这个教程里,我们将通过自定义UIViewController Transition来实现这种效果。过渡(Transition)指的是App在进行界面切换时候的动画,比如显示Modal的动画(从屏幕下方移入)或者Push Segue的动画(从屏幕左侧移入)。就像播放电影一样,在默认情况下,这个动画会从头到尾一口气播放完。

通过自定义过渡,你可以自己设计动画。此外,你还可以让这个动画具有交互性,让它随着你手指的移动播放,就像播放器的进度条一样。

查看图片

 

下载初始项目

点击这里下载初始项目。

starterProjectStoryboard

这个初始项目就是一个简单的Single View Application,包含一个额外的ModalViewControllor(图中绿色表示)。两个界面上各有一个按钮,用于打开/关闭Modal视图。

编译运行,你应该能通过按钮打开或关闭Modal视图。

 

创建自定义Animator

创建自定义过渡的第一步是创建动画。我们需要把这个动画打包进一个叫做Animator的特殊对象里。有些人喜欢把它叫做“Animation Controller”,其实它就是一个遵从UIViewControllerAnimatedTransitioning协议的对象,负责播放你的动画。

interactiveModalDismissal

为了让教程尽可能简单,我们直接模仿关闭Modal视图的默认动画。

 

1. 创建新文件

在项目中新建一个Swift文件,命名为DismissAnimator

 

2. 继承NSObject

把DismissAnimator.swift的内容替换成下面的代码:

	
importUIKit

classDismissAnimator: NSObject{

上述代码创建了一个继承自NSObject的类,遵从NSObjectProtocol协议。

 

3. 实现Animated Transitioning协议

在源文件的末尾对类进行扩展,下面是具体代码:

extension DismissAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
}
}

这个 Animator 采用了 UIViewControllerAnimatedTransitioning 协议,其中包括两个强制实现的方法:

    – transitionDuration(_:):设定动画时长;

    – animateTransition(_:):设定实际动画,需要添加代码。

 

4. 设置播放动画的舞台

在animateTransition(_:)方法里添加下面的guard声明:

guard
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
let containerView = transitionContext.containerView()
else {
return
}

上面的代码展开了几个用于设置动画的对象。

    – fromVC:指Modal View Controller,我们的主角;

    – toVC:指Modal视图后面的父View Controller

    – containerView:把这个想象成设备的屏幕,也就是播放动画的舞台;

默认情况下,动画开始时只显示fromVC,我们需要自行添加toVC

containerView

 

5. 添加目标 View Controller

把下面的代码添加到animationTransition(_:)方法末尾:

containerView.insertSubview(toVC.view, belowSubview: fromVC.view)

它会把父View Controller(toVC)添加到Modal视图的下方。

 

6. 创建动画

往AnimationTransition(_:)方法里再添加些代码:

let screenBounds = UIScreen.mainScreen().bounds
let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
UIView.animateWithDuration(
transitionDuration(transitionContext),
animations: {
fromVC.view.frame = finalFrame
},
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)

上面的代码把fromVC向下移动了一个屏幕的距离。

bottomLeftCorner:首先,我们创建了一个位于屏幕左下角的CGPoint。我们用UIScreen来获得屏幕的高度。

finalFrame:这是Modal视图的最终位置,屏幕可视区域下方一个屏幕的位置。我们把原点(origin)设置为bottomLeftCorner,大小同样通过UIScreen获得。

UIView.animateWithDuration():一个用于展示视图动画的类方法。

transitionDuration(transitionContext):之前我们实现了把动画时长设置为0.6秒的方法,这里只是简单地调用该方法。

animations:在这个块里定义动画。我们把Modal视图从初始位置移动到最终位置。

completeTransition:在自定义动画的最后,我们需要调用completeTransition()方法。在下一节,我们会添加一个Interactor,你可以用它取消动画。我们给completeTransition()传入一个Boolean值,用于标识动画是完成还是回滚取消。

modalDismissalAnimation

迄今为止做的事情

到目前为止,我们创建了一个叫做DismissAnimatorAnimator对象。它遵从UIViewControllerAnimatedTransitioning协议,负责把动画打包成一种特殊格式。

transitionDuration(_:)定义了动画的持续时间。就像视频播放器里的电影长度标签。同样用电影作类比,animateTransition(_:)定义了电影的实际内容。

animateTransition(_:)方法让你能够方便地访问transitionContext。通过transitionContext,你可以获得过渡动画所涉及到的View Controller。你还可以通过它得知动画是否成功完成,方便在动画结束时把实际状态传给completeTransition()方法。

 

连接Animator

对于Modal Segue,默认的关闭动画是向下移出屏幕。你可以通过transitioning delegate改写默认动画。

ViewController.swift的内容替换成下面的代码:

import UIKit
class ViewController: UIViewController {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let destinationViewController = segue.destinationViewController as? ModalViewController {
destinationViewController.transitioningDelegate = self
}
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimator()
}
}

上面的代码做了这样几件事情:

transitioningDelegate:当我们设置了transitioningDelegate后,就手动接管了该View Controller的进入和退出的动画。我们可以在prepareForSegue中进行设置。

animationControllerForDismissedController(_:): 这个方法用我们自定义的动画覆盖了默认动画。

 

编译运行

到目前为止的代码文件

    – ViewController.swift

    – ModalViewController.swift(未修改)

    – DismissAnimator.swift

点击Open Modal按钮,然后点击Close按钮,动画效果应该和初始项目一样。

为了验证我们的确修改了动画,试着把transitionDuration改大一些,比如3.0秒。如果现在动画变得非常缓慢,那么说明我们之前的步骤成功了。

 

创建Interactor

Interactor让你可以通过滑动手势控制动画的进度。对于Modal视图的关闭动画,你可以竖直方向拽动它,它会跟随你的手指。如果你拽的距离足够远,Modal视图会消失。否则他会弹回顶部,取消关闭。

 

1. 创建新文件

创建一个Swift文件,命名为Interactor。

 

2. 继承UIPercentDrivenInteractiveTransition

Interactor.swift里的内容替换成下面的代码:

import UIKit
class Interactor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}

UIPercentDrivenInteractiveTransition: 这个类的作用有点像视频播放器的进度条。它可以把动画的进度设置到特定位置,也可以结束或取消动画。后面会详细介绍。

Interactor子类包含两个作为状态机的标志:

hasStarted:记录用户是否正在交互中。

shouldFinish:决定了过渡动画应该结束还是回滚到初始状态。

 

设置滑动手势(Pan Gesture)

我们使用滑动手势驱动交互式过渡动画。

    a. 在Storyboard里,从Object Library拽一个Pan Gesture RecognizerModal View Controller上。

    b. 按住alt点击ModalViewController.swift,展开并列视图。

    c. 右键(触控板为按住control)把Pan Gesture Recognizer拽到ModalViewController类里。

    d. 在弹出框里把名字设为handleGesture,并把连接设置为Action

    e. 把类型改成UIPanGestureRecognizer并连接。

wirePanGesture

ModalViewController.swift里应该会出现下面的代码:

@IBAction func handleGesture(sender: UIPanGestureRecognizer) {
}

 

连接滑动手势和Interactor

接下来就是见证奇迹的时刻。滑动手势有几种不同的状态,比如.Begin.Ended以及.Changed。你需要解析这些不同的状态,并通过Interactor调用相应的方法。

 

1. 创建Interactor

ModalViewController.swift中添加如下代码:


var interactor:Interactor? = nil

它的父View Controller负责传入这个对象。

 

2. 计算竖直方向拖拽距离

handleGesture(_:)里添加如下代码:

let percentThreshold:CGFloat = 0.3
// convert y-position to downward pull progress (percentage)
let translation = sender.translationInView(view)
let verticalMovement = translation.y / view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)

percentThreshold:这个变量设定了用户需要拖拽多远才能出发Modal的退出动作。在这个例子里,我们设置为30%。

translation:把滑动手势的坐标空间映射到Modal View Controller的坐标空间。

verticalMovement:把竖直方向的距离转化为屏幕高度的百分比。

downwardMovement:捕捉向下滑动的手势,忽略向上滑动。

downwardMovementPercent:把百分比限制在100%。

progress:把百分比转换成CGFloat类型,即Interactor所接受的类型。

 

3. 把滑动手势解析为Interactor的方法调用

handleGesture(_:)里添加如下代码:

guard let interactor = interactor else { return }
switch sender.state {
case .Began:
interactor.hasStarted = true
dismissViewControllerAnimated(true, completion: nil)
case .Changed:
interactor.shouldFinish = progress > percentThreshold
interactor.updateInteractiveTransition(progress)
case .Cancelled:
interactor.hasStarted = false
interactor.cancelInteractiveTransition()
case .Ended:
interactor.hasStarted = false
interactor.shouldFinish
? interactor.finishInteractiveTransition()
: interactor.cancelInteractiveTransition()
default:
break
}

guard:展开可选类型的Interactor

.Begin:用户刚开始拖拽Modal视图。我们调用dismissViewControllerAnimated()方法启动动画,并把hasStarted设置为true

到目前为止,Modal视图每次都必定关闭。后面我们会连接Interactor并检查hasStarted标志,手动控制动画。

.Changed:当用户竖直拖拽时,动画的进度会随着用户拖拽的距离而更新。如果距离超过30%这个阈值,我们就需要完成过渡动画(这里还没有实现,先暂时记着)。

.Cancelled:如果因为某些原因手势取消了,Interactor也调用cancelInteractiveTransition()取消动画。同时,把hasStarted置为false

.Ended:之前已经依据用户拖拽的距离达到阈值与否设定了shouldFinish的值。根据这个标志的具体值,Interactor会相应地调用finishInteractiveTransition()或者cancelInteractiveTransition()。同时,把hasStarted置为false

在这个Interactor里,滑动手势的状态和方法调用是一对一的。滑动手势开始时会启动动画;随着拖拽,Interactor会更新动画的进度;当手势结束时,Interactor要么完成动画,要么回滚动画。

 

连接Interactor

这里的最后一块拼图是连接Interactor。

 

1. 在ViewController.swift里添加下面的代码:


let interactor = Interactor()

这里创建了一个Interactor对象,它会同时被ViewController和ModalViewController使用。

 

2. 用下面代码替换prepareForSegue方法:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let destinationViewController = segue.destinationViewController as? ModalViewController {
destinationViewController.transitioningDelegate = self
destinationViewController.interactor = interactor // new
}
}

上面代码把Interactor对象传递给了ModalViewController,使两个Controller使用同一个状态机。

 

3. 把下面的方法添加到UIViewControllerTransitioningDelegate扩展:

func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}

hasStarted决定了动画是否进入交互模式。当用户滑动时会返回Interactor,但如果用户只是点击Close按钮,会播放普通的动画。

 

编译运行

到目前为止的代码文件

    – ViewController.swift

    – ModalViewController.swift

    – DismissAnimator.swift(未修改)

点击Open Modal按钮。

把Modal视图往下拽一点点就松开,它会弹回顶部。

把Modal视图往下拽到一半左右并松开,它会执行剩下的动画向下移动离开屏幕。

wiredInteractor

 

添加手势提示(coach mark)

用户需要你的提示才能知道隐藏的手势操作。动画是一种简洁的传递这个信息的方式,同时它还不会妨碍到App的使用。在这一节,我们会创建一个向下移动的白色的圆圈,用来代表用户可以使用的手势。

 

把下面代码添加到ModalViewController.swift

func showHelperCircle(){
let center = CGPoint(x: view.bounds.width * 0.5, y: 100)
let small = CGSize(width: 30, height: 30)
let circle = UIView(frame: CGRect(origin: center, size: small))
circle.layer.cornerRadius = circle.frame.width/2
circle.backgroundColor = UIColor.whiteColor()
circle.layer.shadowOpacity = 0.8
circle.layer.shadowOffset = CGSizeZero
view.addSubview(circle)
UIView.animateWithDuration(
0.5,
delay: 0.25,
options: [],
animations: {
circle.frame.origin.y += 200
circle.layer.opacity = 0
},
completion: { _ in
circle.removeFromSuperview()
}
)
}
override func viewDidAppear(animated: Bool) {
showHelperCircle()
}

这是一段简单粗犷的展示动画的方式。我们通过代码直接在屏幕顶部创建了一个圆圈。我们通过cornerRadiusbackgroundColorshadowOpacity以及shadowOffset属性设置它的样式。圆圈在向下移动的同事会逐渐消失。

 

编译运行

到目前为止的代码文件

    – ViewController.swift

    – ModalViewController.swift

    – DismissAnimator.swift(未修改)

每当Modal视图出现的时候,你都会看到一个白色的小圆圈向下移动。

interactiveModalDismissal

 

总结

你可以在这里下载完整项目。

交互式移除Modal视图只是自定义View Controller Transition API的众多应用场景之一。如果你想了解更多,我强烈推荐Ray Wenderlich的这篇教程(译者注:以后会进行翻译)。

译者:在实际使用过程中,可以把Interactor和Animator抽取出来,集成到项目里作为Utility类使用,几行代码就可以为你的项目添加向下滑动关闭Modal视图的手势。

Screen Shot 2016-07-14 at 11.37.48 PM

原文链接:http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/

下拉手势关闭Modal视图