iOS9-自定义转场

发布于:2023-03-12 ⋅ 阅读:(94) ⋅ 点赞:(0)

参考书籍:iOS 9 by Tutorials

博客原文

其实早在iOS7就推出了两个View之间的自定义过度转变。但是在iOS9中这种自定义转换进一步让你通过自定义segues来使过渡动画和视图控制器完全分离。

通过一个小的demo来了解一下吧。

Getting started

初始化代码

一个简单的项目PamperedPets,宠物照看的应用程序,完成后讲显示宠物的思想和她们的详细列表。

试着探索一下这个初始项目,看他是怎么运行的。

注意:当你打开这个项目的时候在Storyboard中会有些警告,不要惊慌。之后会解决的。

看一下Main.storyboard它有一些预先创建好的scenes,你将开始Animal Detail and Animal Photo scenes的工作。

What are segues?

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

A simple segue

Main.storyboard里选择Animal Detail View Controller,从Object Library拖拽出一个Tap Gesture Recognizer放在Pet Photo Thumbnail上。

接下来,Ctrl-dragTap Gesture Recognizer TO Tap Gesture Recognizer ,从弹出的菜单中选择present modally

完成上边的步骤就建立好了一个segue

选择 Animal Detail View ControllerAnimal 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 modalAnimalDetailViewControllerAnimalPhotoViewControllerAnimalDetailViewController在这里被称作为source view controller,而AnimalPhotoViewController责备称作为destination view controller。这个segue持有sourcedestination 的引用。

在这个过程中,目标视图控制器将会调用transition delegate来执行默认的Cover Vertical动画。

Your custom segue library

Main.storyboard中选择 PhotoDetail segue( the Animal Detail and the Animal Photo view controllers.)改变他的segue class DropSegue

再次运行项目,你会发现点击照片之后的动画已经完全改变了。

Creating a custom segue

现在你创建一个自己定义的的segue去更换DropSegue。并且将要创建一个如下图的转场动画.

创建一个自定义的seuge最难的部分就是术语,你将要使用的协议名字相当的长。

  • UIViewControllerTransitioningDelegate : 自定义转场使用该协议来完成动画。

  • UIViewControllerAnimatedTransitioning: 该动画对象通过该协议来描述动画。

  • UIViewControllerContextTransitioning: 这个上下文包含有关呈现,并介绍控制器和视图的详细信息;你把它传递给动画对象,为他们提供在其上执行动画的背景下。

在你开始之前,我们先看看创建一个转场的动画都需要那些步骤:

  1. 继承UIStoryboardSegue的子类,设置segue 为目标控制器的委托.

  2. 创建一个展示和消失的animator

  3. 定义动画效果及其持续时间,在动画中使用。

  4. 指导segue用于演示和解雇动画类。

  5. 最后在故事版中使用这个segue

Subclass UIStoryboardSegue

创建一个Cocoa Touch Class文件命名为ScaleSegue.swift继承UIStoryboardSegue

然后扩展这个类

extension ScaleSegue: UIViewControllerTransitioningDelegate {
}

ScaleSegue这个类里重写父类的方法preform()

override func perform() {
  destinationViewController.transitioningDelegate = self
  super.perform()
}

在这里你设置destination view controllertransitioning delegateScaleSegue

Create the animator

ScaleSegue.swift文件下边写如下一个类:

class ScalePresentAnimator : NSObject,
 UIViewControllerAnimatedTransitioning {
}

你将使用ScalePresentAnimator这个类去展现modal view 。你也将建立一个消失时的动画,但是目前来说,一切都还是使用的默认的动画。需要注意的是Xcode中会抱怨这还不符合UIViewControllerAnimatedTransitioning协议;你只是要解决这个问题。

Define the animation

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)
        }
        
    }

Set the animator in the segue

UIViewControllerTransitioningDelegate下添加下边这个方法。

extension ScaleSegue:UIViewControllerTransitioningDelegate{
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return ScalePresentAnimator()
    }
}
这是简单的在告诉`segue`在展现下一个view的时候使用你的`ScalePresentAnimator`动画

Use the segue in the storyboard

接下来在Main.storyboard中将PhotoDetail segue更换成ScaleSegue,同时呢,改变Presentation成为Form Sheet

接下来运行程序你就会看到下边的动画。

Passing data to animators

通过协议来传递数据。在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你就会发现当你单击图片之后,图片就会从原本的位置满满地放大。是不是这样子看起来更加的舒服呢?

Working with the view hierarchy

接下来我们做点小的改变来让你的app在iPad上运行起来别具一格。

找到animateTransition(_:)这个函数,在` toView?.frame = finalFrame
toView?.layoutIfNeeded()`后边紧跟着写上下边的代码

 fromView?.alpha = 0.0

然后在动画完成的闭包里写上:

 fromView?.alpha = 1.0
 transitionContext.completeTransition(true)

接下来在你的iPad中运行你的app,你会看到下边的样子。

Handling embedded view controllers

接下来呢我们在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处理情况也是类似的。

Completing the scale segue dismissal

你会发现如果再次点击大图,大图消失的时候的动画还是之前的默认情况。我们接下来就完成消失时的动画吧。其实呢既然已经完成了presenting animator那么dismiss animator就简单了许多了吧。道理是一样的,那你就挑战一下自己吧。完成接下来的任务!

修改下边这个段代码

if let toView = toView{
            transitionContext.containerView()?.addSubview(toView)
        }

修改为

if let toView = toView,fromView = fromView{
           transitionContext.containerView()?.insertSubview(toView, belowSubview: fromView)
       }
本文含有隐藏内容,请 开通VIP 后查看