参考书籍: iOS 9 by Tutorials
博客原文
其实早在iOS7就推出了两个View之间的自定义过度转变。但是在iOS9中这种自定义转换进一步让你通过自定义 segues
来使过渡动画和视图控制器完全分离。
通过一个小的demo来了解一下吧。
初始化代码
一个简单的项目 PamperedPets
,宠物照看的应用程序,完成后讲显示宠物的思想和她们的详细列表。
试着探索一下这个初始项目,看他是怎么运行的。
注意:当你打开这个项目的时候在 Storyboard
中会有些警告,不要惊慌。之后会解决的。
看一下 Main.storyboard
它有一些预先创建好的 scenes
,你将开始 Animal Detail
and Animal Photo
scenes的工作。
Segues
描述了场景之间的转换。他们显示为视图控制器场景之间的箭头,有几种类型的 Segues
Show : Pushes a scene from a navigation controller.
Show Detail: Replaces a scene detail when in a UISplitViewController
Present Modally: Presents a scene on top of the current scene.
Popover: Presents a scene as a popover on the iPad or full screen on the iPhone.
这篇文章我们仅仅自定义 modal segues
在 Main.storyboard
里选择 Animal Detail View Controller
,从 Object Library
拖拽出一个 Tap Gesture Recognizer
放在 Pet Photo Thumbnail
上。
接下来, Ctrl-drag
从 Tap Gesture Recognizer
TO Tap Gesture Recognizer
,从弹出的菜单中选择 present modally
完成上边的步骤就建立好了一个 segue
。
选择 Animal Detail View Controller
和 Animal Photo View Controller
之间的 segue
.奖 identifier
设置为 PhotoDetail
在 AnimalDetailViewController.swift
中重写 prepareForSegue(_:sender:)
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "PhotoDetail" { let controller = segue.destinationViewController as! AnimalPhotoViewController controller.image = imageView.image } }
现在你运行app并且点击照片,你将会看到一个大的图片出现。
你会发现你回不去了。那么此时你需要创建一个 unwind segue
。在 AnimalDetailViewController.swift:
中添加如下代码:
@IBAction func unwindToAnimalDetailViewController( segue:UIStoryboardSegue) { // placeholder for unwind segue }
对于一个简单的返场,在这个方法里你不需要添加任何的代码。 任何类似于这样的方法 @IBAction func methodName(segue:UIStoryboardSegue)
都会被认为是 Storyboard segue 的 unwind
在 Main.storyboard
中选择 Animal Photo View Controller scene.
。从 Object Library
拖出来一个 Tap Gesture Recognizer
放在 Pet Photo View
上。接下来, Ctrl-drag
从你的 Tap Gesture Recognizer
TO Exit
,之后从弹出的菜单中选择 unwindToAnimalDetailViewController
重新运行app,就会回发现你从大的图片中返回去了。
我们来探究一下这里发生了什么。当你点击详细视图中的缩略图的时候,手势识别就开始一个 segue modal
从 AnimalDetailViewController
到 AnimalPhotoViewController
。 AnimalDetailViewController
在这里被称作为 source view controller
,而 AnimalPhotoViewController
责备称作为 destination view controller
。这个 segue
持有 source
和 destination
的引用。
在这个过程中,目标视图控制器将会调用 transition delegate
来执行默认的 Cover Vertical
动画。
在 Main.storyboard
中选择 PhotoDetail segue
( the Animal Detail and the Animal Photo view controllers.
)改变他的 segue class
为 DropSegue
再次运行项目,你会发现点击照片之后的动画已经完全改变了。
现在你创建一个自己定义的的 segue
去更换 DropSegue
。并且将要创建一个如下图的转场动画.
创建一个自定义的seuge最难的部分就是术语,你将要使用的协议名字相当的长。
UIViewControllerTransitioningDelegate : 自定义转场使用该协议来完成动画。
UIViewControllerAnimatedTransitioning: 该动画对象通过该协议来描述动画。
UIViewControllerContextTransitioning: 这个上下文包含有关呈现,并介绍控制器和视图的详细信息;你把它传递给动画对象,为他们提供在其上执行动画的背景下。
在你开始之前,我们先看看创建一个转场的动画都需要那些步骤:
继承 UIStoryboardSegue
的子类,设置 segue
为目标控制器的委托.
创建一个展示和消失的 animator
类
定义动画效果及其持续时间,在动画中使用。
指导 segue
用于演示和解雇动画类。
最后在故事版中使用这个 segue
创建一个 Cocoa Touch Class
文件命名为 ScaleSegue.swift
继承 UIStoryboardSegue
。
然后扩展这个类
extension ScaleSegue: UIViewControllerTransitioningDelegate { }
在 ScaleSegue
这个类里重写父类的方法 preform()
override func perform() { destinationViewController.transitioningDelegate = self super.perform() }
在这里你设置 destination view controller
的 transitioning delegate
是 ScaleSegue
。
在 ScaleSegue.swift
文件下边写如下一个类:
class ScalePresentAnimator : NSObject, UIViewControllerAnimatedTransitioning { }
你将使用 ScalePresentAnimator
这个类去展现 modal view
。你也将建立一个消失时的动画,但是目前来说,一切都还是使用的默认的动画。需要注意的是Xcode中会抱怨这还不符合UIViewController AnimatedTransitioning
协议;你只是要解决这个问题。
ScalePresentAnimator
遵从 UIViewControllerAnimatedTransitioning
,你需要实现这个协议所必需的一些方法。
func transitionDuration( transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { return 2.0 } //规定动画持续的时间(一般时间比较短,这里设置的比较长,是为了明显的看到效果)
实际的动画效果:
func animateTransition(transitionContext: UIViewControllerContextTransitioning){ // 1 获取到目标视图的控制器和View let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! let toView = transitionContext.viewForKey(UITransitionContextFromViewKey) // 2 添加 toView到transiton的context if let toView = toView{ transitionContext.containerView()?.addSubview(toView) } //3 目标视图的初始状态是在屏幕左上角大小为零的一个矩形,当你更改视图的 Frame 时总是要去调用`layoutIfNeeded`来更新视图的约束 toView?.frame = .zero toView?.layoutIfNeeded() //4 这个动画必报就是把那个大小为零的矩形View变成最终的大小的一个动画 let duration = transitionDuration(transitionContext) let finalFrame = transitionContext.finalFrameForViewController(toViewController) UIView.animateWithDuration(duration, animations: { () -> Void in toView?.frame = finalFrame toView?.layoutIfNeeded() }) { (_) -> Void in //5 transitionContext要在动画结束时清理,调用completeTransition transitionContext.completeTransition(true) } }
在 UIViewControllerTransitioningDelegate
下添加下边这个方法。
extension ScaleSegue:UIViewControllerTransitioningDelegate{ func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return ScalePresentAnimator() } } 这是简单的在告诉`segue`在展现下一个view的时候使用你的`ScalePresentAnimator`动画
接下来在 Main.storyboard
中将 PhotoDetail segue
更换成 ScaleSegue
,同时呢,改变 Presentation
成为 Form Sheet
接下来运行程序你就会看到下边的动画。
通过协议来传递数据。在 ScaleSegue.swift
里建立一个 protocol
protocol ViewScaleable{ var scaleView:UIView{get} }
通过扩展 AniamalDetailViewController
继承 ViewScaleable
协议
在 AniamalDetailViewController.swift
中添加下边的代码
extension AniamalDetailViewController:ViewScaleable{ var scaleView: UIView {return imageView} }
在 ScaleSegue.swift
文件中找到 animateTransiton
这个函数,在 let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
下添加如下代码
//获取源视图的控制器和View let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
将 toView?.frame = .zero
替换为
var startFrame = CGRect.zero if let fromViewController = fromViewController as? ViewScaleable{ startFrame = fromViewController.scaleView.frame }else{ print("Warning: Controller /(fromViewController) does not"+"conform to ViewScaleable") } toView?.frame = startFrame
现在你重新运行你的app你就会发现当你单击图片之后,图片就会从原本的位置满满地放大。是不是这样子看起来更加的舒服呢?
接下来我们做点小的改变来让你的app在iPad上运行起来别具一格。
找到 animateTransition(_:)
这个函数,在` toView?.frame = finalFrame
toView?.layoutIfNeeded()`后边紧跟着写上下边的代码
fromView?.alpha = 0.0
然后在动画完成的闭包里写上:
fromView?.alpha = 1.0 transitionContext.completeTransition(true)
接下来在你的iPad中运行你的app,你会看到下边的样子。
接下来呢我们在 Main.storyboard
中选择 Navigation Controller
,在属性面板中勾选上 Is Initial View Controller:
这一项。
现在呢你运行程序你会首先看到一个动物的列表,你任意的点击一个,然后点击图片你会发现。奇怪怎么又变成了从左上角出现的动画了。
那是应为我把视图控制器现在嵌入了导航控制器里,使得呈现视图控制器的不是 AnimalDetailViewController
那很简单我们来解决一下就好了。
我们在 ScaleSegue.swift
这个文件里找到,
let fromViewController = transitionContext .viewControllerForKey( UITransitionContextFromViewControllerKey)!
将这句代码改为:
var fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! if let fromNC = fromViewController as? UINavigationController{ if let controller = fromNC.topViewController{ fromViewController = controller } }
此刻你重新运行代码就会恢复原样喽。
当你嵌入了一个 UITaBarController
处理情况也是类似的。
你会发现如果再次点击大图,大图消失的时候的动画还是之前的默认情况。我们接下来就完成消失时的动画吧。其实呢既然已经完成了 presenting animator
那么 dismiss animator
就简单了许多了吧。道理是一样的,那你就挑战一下自己吧。完成接下来的任务!
修改下边这个段代码
if let toView = toView{ transitionContext.containerView()?.addSubview(toView) }
修改为
if let toView = toView,fromView = fromView{ transitionContext.containerView()?.insertSubview(toView, belowSubview: fromView) }