swift自定义presentViewController动画和dismiss(视图控制器切换动画)

我们在左一些app的时候经常会用到详情页,评价页 , 总之就是点一个按钮 就展示一些信息,在做一些简单的展示或者小逻辑。一般都会presentViewController。默认的动画是从下往上,但是我们想要自己主宰它的动画方式怎么弄呢?

swift自定义presentViewController动画和dismiss(视图控制器切换动画)_第1张图片
图片来自网络

本小节将会以一个点击获取英雄详情的demo来介绍一个自定义presentViewController动画(视图控制器切换动画)

本文源码:https://github.com/smalldu/IOS-Animations
中的AnimationDemo11    

简单的效果

简单的效果

进阶效果

进阶效果

我们在左一些app的时候经常会用到详情页,评价页 , 总之就是点一个按钮 就展示一些信息,在做一些简单的展示或者小逻辑。一般都会presentViewController

大家可以下载我的代码,看看一些跟过渡动画没有关系的设置,比如文字,和半透明背景 ,下面UIScrollView等等 , 因为他们不是本节要介绍的重点,本节要介绍的重点是自定义过渡动画。

首先,创建一个Single View Application,然后在Main.storyboard中定义好搞两个界面,定义好约束 。 不懂的可以下载我源码。看源码上,也可以不搞这么复杂,随便搞两个页面 练习过渡动画就行。

我的页面结构

swift自定义presentViewController动画和dismiss(视图控制器切换动画)_第2张图片
页面构建

页面所有元素都是基于AutoLayout约束的

然后就是创建了一盒Hero.swift 用于存放英雄的基本信息 , 然后在ViewController中将这些英雄的图像加到UIScrollView上,计算好他们的位置。

每个图像都添加 imageView.userInteractionEnabled = true 属性,可交互,然后添加点击的手势

imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("didTapImageView:")))

然后就是在点击的时候展示详情页 , 详情页。就会传一个Hero对象的参数。没有什么非常特别的。

 func didTapImageView(tap: UITapGestureRecognizer) {
        selectedImage = tap.view as? UIImageView
        
        let index = tap.view!.tag
        let selectedHerb = he[index-1]
        //present details view controller
        let details = storyboard?.instantiateViewControllerWithIdentifier("detailViewController") as! DetailViewController
        details.he = selectedHerb
        presentViewController(details, animated: true, completion: nil)
    }

这里我把第二个控制器的设置如图

swift自定义presentViewController动画和dismiss(视图控制器切换动画)_第3张图片
t4.png

这时候执行还是默认的效果

swift自定义presentViewController动画和dismiss(视图控制器切换动画)_第4张图片
默认效果

如果你的控制器要设置自己的动画需要实现UIViewControllerTransitioningDelegate协议。每次你present一个新的ViewController的时候,UIKit就会看这个delegate是否使用自定义过渡。

UIKit通过调用
animationControllerForPresentedController(:_presentingController:sourceController:);方法,如果这个方法返回nil ,就会调用默认的present 。如果返回的是一个非空对象,就会使用这个对象的控制过渡 ,这个对象必须是实现UIViewControllerAnimatedTransitioning协议的对象。

UIViewControllerAnimatedTransitioning这个有两个必须的方法

  • transitionDuration 这个方法需要提供返回一个时间,动画持续时间

  • animateTransition 这个是动画的主体方法

我们新建一个PopAnimation.swift的类让它继承NSObject ,然后实现UIViewControllerAnimatedTransitioning协议 实现了协议就自然要实现那两个方法,第一个方法简单返回一个时间就行了,这里暂且返回1。

第二个方法有传一个参数 transitionContext: UIViewControllerContextTransitioning

UIViewControllerContextTransitioning是个什么东西呢 ?
当两个ViewController之间过渡的时候,刚开始新的控制器已经被创建只是还不可见,因此你的任务就是在animateTransition()方法中把新的控制器添加到transition的容器中,把它以动画的方式添加进来 , 把旧控制器以动画方式移除去

transitionContext提供两个非常便捷的方法让你获得transition对象:

  • viewForKey() :这个可以通过UITransitionContextFromViewKey 和
    UITransitionContextToViewKey 获得新旧视图]
  • viewControllerForKey(): UITransitionContextFromViewControllerKey 和
    UITransitionContextToViewControllerKey 获得新旧试图控制器

所以 我们这里先上一个最简单的动画

import UIKit

class PopAnimator: NSObject,UIViewControllerAnimatedTransitioning {

    let duration = 1.0
    //动画持续时间
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return duration
    }
    
    //动画执行的方法
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        let containerView = transitionContext.containerView()
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
        containerView!.addSubview(toView!)
        toView!.alpha = 0.0
        UIView.animateWithDuration(duration,
            animations: {
                toView!.alpha = 1.0
            }, completion: { _ in
                transitionContext.completeTransition(true)
        })
    }
    
}

将它的不透明度由0变为1 ,然后在完成的时候调用动画完成方法
我们首先在ViewController中声明let transition = PopAnimator()

我们前面说了,我们的ViewController还需要实现UIViewControllerTransitioningDelegate协议

为了代码整洁,我们在ViewController最下面添加

extension ViewController:UIViewControllerTransitioningDelegate{
   
    //Present的时候 使用自定义的动画
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        return transition
    }
    
    //使用默认的动画
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
    }
}

这两个代理方法,第一个是Presentpresent返回我们自定义的,第二个暂且返回默认的。

最后别忘了加代理


 func didTapImageView(tap: UITapGestureRecognizer) {
        selectedImage = tap.view as? UIImageView
        
        let index = tap.view!.tag
        let selectedHerb = he[index-1]
        //present details view controller
        let details = storyboard?.instantiateViewControllerWithIdentifier("detailViewController") as! DetailViewController
        print(details)
        details.he = selectedHerb
        details.transitioningDelegate = self //设置过渡代理
        presentViewController(details, animated: true, completion: nil)
    }

效果

t2.gif

这样一个简单的效果就实现了。

要实现复杂的效果,我们需要一些计算。首先在PopAnimation中新增两个变量

    var presenting = true  //是否在presenting 
    var originFrame = CGRect.zero

presenting主要用来区分是present还是dismiss

然后在animateTransition 中

        let containerView = transitionContext.containerView()
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
        let detailView = presenting ? toView :
            transitionContext.viewForKey(UITransitionContextFromViewKey)!

前两个没变,后面一个变了,如果当前是present , detail就是toView如果不是detail就是from 。

然后加上下面三句

 let initialFrame = presenting ? originFrame : detailView!.frame
 let finalFrame = presenting ? detailView!.frame : originFrame
 let xScaleFactor = presenting ?
            initialFrame.width / finalFrame.width :
            finalFrame.width / initialFrame.width

let yScaleFactor = presenting ?
                initialFrame.height / finalFrame.height :
                finalFrame.height / initialFrame.height

如果是present初始就是originFrame原如果不是初始就是detail的frame 。final同理

最后那个算出现在缩放比例

然后添加代码

 let scaleTransform = CGAffineTransformMakeScale(xScaleFactor,
            yScaleFactor)
  if presenting {
            detailView!.transform = scaleTransform
            detailView!.center = CGPoint(
            x: CGRectGetMidX(initialFrame),
            y: CGRectGetMidY(initialFrame))
            detailView!.clipsToBounds = true
 }

定义一个变换,如果present的话 先把detail先缩放(按照山上面计算的比例缩放),然后设置center。为了定位到当前点击的小图的位置。

最后一段


  containerView!.addSubview(toView!)
       containerView!.bringSubviewToFront(detailView!)
        
        UIView.animateWithDuration(duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: UIViewAnimationOptions.AllowAnimatedContent, animations: {
                detailView!.transform = self.presenting ?
                CGAffineTransformIdentity : scaleTransform
                detailView!.center = CGPoint(x: CGRectGetMidX(finalFrame),
                y: CGRectGetMidY(finalFrame))
                
            }) { (_) -> Void in
                    
                transitionContext.completeTransition(true)
        }

第一句无可厚非,为啥要加第二句呢?containerView!.bringSubviewToFront(detailView!)

因为如果是present的话本来就应该放在最前面 ,如果是dismiss的话,不放在最前面开不到变小的效果。

最后动画如果是present就动画还原detail , 如果是dismiss 就把detail缩放,设置center 。

这时候你运行代码 , 并没有从图哪里扩大,而使从0,0点 。

因为这里还有一件事情要做,转换坐标。

在ViewController中

//Present的时候 使用自定义的动画
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
       
        transition.originFrame =
            selectedImage!.superview!.convertRect(selectedImage!.frame,
            toView: nil)
        transition.presenting = true
        selectedImage!.hidden = true

        return transition
    }

  //使用默认的动画
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        selectedImage!.hidden = false
        transition.presenting = false
        return transition
    }

第一行是把选择的试图的坐标转换成父试图的坐标,然后transition.presenting 设置了状态,还隐藏了选择的图,这个是为了。dismiss的时候,下面没有图,dismis完成的时候这个图才能显示 。

这个也很简单,在动画完成的时候判断下,如果是dismiss就执行一段代码就行了,可以用代理我这里直接用了闭包

声明一个闭包在PopAnimation

var hideImage:(()->())?

然后动画完成

  if !self.presenting{
                    self.hidIt()
                }

   func hidIt(){
        hideImage?()
    }

最后viewc中dsmiss的时候加上那段就行了

 //使用默认的动画
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        transition.hideImage={
            self.selectedImage!.hidden = false
        }
        transition.presenting = false
        return transition
    }

效果

最终效果

到这里 基本就实现了,主要是后面的算,其实自定义过渡没啥。就那几步。希望大家从中能学到一些东西,这个动画还能更完善就是dismiss之后的圆角。看官们自己搞搞吧

你可能感兴趣的:(swift自定义presentViewController动画和dismiss(视图控制器切换动画))