iOS swift 自定义页面的切换动画与交互动画

参照:iOS 自定义页面的切换动画与交互动画 By Swift
oc 版切换动画

一、自定义导航栏的Push/Pop动画

为了在基于UINavigationController下做自定义的动画切换,
1、先建立一个简单的工程,建一个UINavigationController的子类LSNavigationController,另外两个VC viewControllersecondViewController,注意:viewController是一个UINavigationController。在这两个页面中先做一些准备工作就是各有一个按钮,一个做push操作,一个做pop操作。
2、LSNavigationController ,用来实现UINavigationControllerDelegate```协议。在类中实现代理函数

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if operation == UINavigationControllerOperation.push {
            return LSPushAnimation.init()
        }else if operation == UINavigationControllerOperation.pop {
            return LSPopAnimation.init()
        }
        return nil
    }

上面的协议函数会在push和pop时返回已经实现了动画方法的类LSPushAnimationLSPopAnimation
viewControllerviewDidLoadself.navigationController?.delegate = self,因为导航器的第一个页面一直存在,所以只需要在这里设置就可。
3、编写动画类,由于pop和push实现方式类似,只拿push为例:
LSPushAnimation中实现UIViewControllerAnimatedTransitioning协议。 UIViewControllerAnimatedTransitioning是苹果新增加的一个协议,其目的是在需要使用自定义动画的同时,又不影响视图的其他属性,让你把焦点集中在动画实现的本身上,然后通过在这个协议的回调里编写自定义的动画代码,即“切换中应该会发生什么”,负责切换的具体内容,任何实现了这一协议的对象被称之为动画控制器。
实现两个协议函数

//UIViewControllerAnimatedTransitioning
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.4
    }
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        
        var destView: UIView!
        var destTransform: CGAffineTransform!
        containerView.insertSubview((toViewController?.view)!, aboveSubview: (fromViewController?.view)!)
        destView = toViewController?.view
        destView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
        destTransform = CGAffineTransform(scaleX: 1, y: 1)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            destView.transform = destTransform
        }, completion: ({completed in
            transitionContext.completeTransition(true)
        }))
    }

上面第一个方法返回动画持续的时间,而下面这个方法才是具体需要实现动画的地方。UIViewControllerAnimatedTransitioning的协议都包含一个对象:transitionContext,通过这个对象能获取到切换时的上下文信息,比如从哪个VC切换到哪个VC等。我们从transitionContext获取containerView,这是一个特殊的容器,切换时的动画将在这个容器中进行;UITransitionContextFromViewControllerKeyUITransitionContextToViewControllerKey就是从哪个VC切换到哪个VC,容易理解;除此之外,还有直接获取view的UITransitionContextFromViewKeyUITransitionContextToViewKey等。
我按Push和Pop把动画简单的区分了一下,Push时scale由小变大,Pop时scale由大变小,不同的操作,toViewController的视图层次也不一样。最后,在动画完成的时候调用completeTransition,告诉transitionContext你的动画已经结束,这是非常重要的方法,必须调用。在动画结束时没有对containerView的子视图进行清理(比如把fromViewController的view移除掉)是因为transitionContext会自动清理,所以我们无须在额外处理。

4、这样,只需在

注意一点,这样一来会发现原来导航栏的交互式返回效果没有了,如果你想用原来的交互式返回效果的话,在返回动画控制器的delegate方法里返回nil,如:

if operation == UINavigationControllerOperation.Push {
    navigationOperation = operation
    return self
}
return nil

然后在LSNavigationControllerviewDidLoad里,Objective-C直接self.navigationController.interactivePopGestureRecognizer.delegat = self就行了,Swift除了要navigationController.interactivePopGestureRecognizer.delegate = self之外,还要在self上声明实现了UIGestureRecognizerDelegate这个协议,虽然实际上你并没有实现。
一个简单的自定义导航栏Push/Pop动画就完成了。

二、自定义Modal的Present/Dismiss动画

自定义Modal的Present与Dismiss动画与之前类似,都需要提供一个动画管理器,我们用详情页面来展示一个Modal页面,详情页面就作为动画管理器:
为了方便,我依然在LSViewController操作(实际开发中,只需要在有特殊需要的页面中实现即可),
1、 LSViewController实现协议UIViewControllerTransitioningDelegate,这个协议与之前的UINavigationControllerDelegate协议具有相似性,都是返回一个动画管理器,

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return LSPresentAnimation.init()
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return LSDismissAnimation.init()
    }

其中LSPresentAnimationLSDismissAnimation也是实现了UIViewControllerAnimatedTransitioning协议的用来实现具体的动画。直接上代码。

//UIViewControllerAnimatedTransitioning
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6
    }
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        
        var destView: UIView!
        var destTransfrom = CGAffineTransform()
        let screenHeight = UIScreen.main
            .bounds.size.height
        
        
        destView = toViewController?.view
        destView.transform = CGAffineTransform(translationX: 0, y: screenHeight)
        containerView.addSubview((toViewController?.view)!)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0,
                       options: UIViewAnimationOptions.curveLinear, animations: {
                        destView.transform = destTransfrom
        }, completion: {completed in
            transitionContext.completeTransition(true)
        })
        
    
    }
3、在 LSViewControllerself.transitioningDelegate = self(之所以让viewControllersecondViewcontroller继承LSViewController是因为懒,都写在一起了,不然需要在两个VC中实现)

这样present就会有动画,想要dismiss也实现我们自己的动画需要在viewControllerpresent按钮中将 secondViewController定位动画管理器。

以上简单demo可以点击下载

三、自定义导航栏的交互式动画

与动画控制器类似,我们把实现了*** UIViewControllerInteractiveTransitioning 协议的对象称之为交互控制器***,最常用的就是把交互控制器应用到导航栏的Back手势返回上,而如果要实现一个自定义的交互式动画,我们有两种方式来完成:实现一个交互控制器,或者使用iOS提供的UIPercentDrivenInteractiveTransition类作交互控制器。

使用UIPercentDrivenInteractiveTransition

我们这里就用UIPercentDrivenInteractiveTransition来完成导航栏的交互式动画。先看下UIPercentDrivenInteractiveTransition的定义:

open class UIPercentDrivenInteractiveTransition : NSObject, UIViewControllerInteractiveTransitioning {

    
    /// This is the non-interactive duration that was returned when the
    /// animators transitionDuration: method was called when the transition started.
    open var duration: CGFloat { get }

    
    /// The last percentComplete value specified by updateInteractiveTransition:
    open var percentComplete: CGFloat { get }

    
    /// completionSpeed defaults to 1.0 which corresponds to a completion duration of
    /// (1 - percentComplete)*duration.  It must be greater than 0.0. The actual
    /// completion is inversely proportional to the completionSpeed.  This can be set
    /// before cancelInteractiveTransition or finishInteractiveTransition is called
    /// in order to speed up or slow down the non interactive part of the
    /// transition.
    open var completionSpeed: CGFloat

    
    /// When the interactive part of the transition has completed, this property can
    /// be set to indicate a different animation curve. It defaults to UIViewAnimationCurveEaseInOut.
    /// Note that during the interactive portion of the animation the timing curve is linear. 
    open var completionCurve: UIViewAnimationCurve

    
    /// For an interruptible animator, one can specify a different timing curve provider to use when the
    /// transition is continued. This property is ignored if the animated transitioning object does not
    /// vend an interruptible animator.
    @available(iOS 10.0, *)
    open var timingCurve: UITimingCurveProvider?

    
    /// Set this to NO in order to start an interruptible transition non
    /// interactively. By default this is YES, which is consistent with the behavior
    /// before 10.0.
    @available(iOS 10.0, *)
    open var wantsInteractiveStart: Bool

    
    /// Use this method to pause a running interruptible animator. This will ensure that all blocks
    /// provided by a transition coordinator's notifyWhenInteractionChangesUsingBlock: method
    /// are executed when a transition moves in and out of an interactive mode.
    @available(iOS 10.0, *)
    open func pause()

    
    // These methods should be called by the gesture recognizer or some other logic
    // to drive the interaction. This style of interaction controller should only be
    // used with an animator that implements a CA style transition in the animator's
    // animateTransition: method. If this type of interaction controller is
    // specified, the animateTransition: method must ensure to call the
    // UIViewControllerTransitionParameters completeTransition: method. The other
    // interactive methods on UIViewControllerContextTransitioning should NOT be
    // called. If there is an interruptible animator, these methods will either scrub or continue 
    // the transition in the forward or reverse directions.
    open func update(_ percentComplete: CGFloat)

    open func cancel()

    open func finish()
}

实际上这个类就是实现了UIViewControllerInteractiveTransitioning协议的交互控制器,我们使用它就能够轻松地为动画控制器添加一个交互动画。调用update更新进度;调用cancel取消交互,返回到切换前的状态;调用finish通知上下文交互已完成,同completeTransition一样。我们把交互动画应用到详情页面Back回主页面的地方,由于之前的动画管理器的角色是主页面担任的,Navigation Controllerdelegate同一时间只能有一个。

首先我们需要创建一个交互控制器。新建一个Cocoa Touch Class文件,命名为LSPercentDrivenInteractiveTransition,让它继承自UIPercentDrivenInteractiveTransition

打开LSNavigationController.swift,在类定义的最开始添加下面这些属性:

var interactionInProgress = false //用于指示交互是否在进行中。
    
    ///交互控制器
    private var interactivePopTransition : LSPercentDrivenInteractiveTransition!

在viewDidLoad:中添加

self.delegate = self
        let gesture = UIScreenEdgePanGestureRecognizer(target:self,action:#selector(handleGesture(gestureRecognizer:)))
        gesture.edges = .left
        self.view.addGestureRecognizer(gesture)

并实现手势的方法:

 // 以下----使用UIPercentDrivenInteractiveTransition交互控制器
        func handleGesture(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
            var progress = gestureRecognizer.translation(in: gestureRecognizer.view?.superview).x / self.view.bounds.size.width
            progress = min(1.0, max(0.0, progress))
    //        let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview)
    //        var progress = Float(translation.x / 200)
    //        progress = fminf(fmaxf(progress, 0.0), 1.0)
    
            print("\(progress)")
            if gestureRecognizer.state == UIGestureRecognizerState.began {
                print("Began")
                self.interactivePopTransition = LSPercentDrivenInteractiveTransition()
                interactionInProgress = true
                self.popViewController(animated: true)
            } else if gestureRecognizer.state == UIGestureRecognizerState.changed {
                self.interactivePopTransition.update(CGFloat(progress))
                print("Changed")
            } else if gestureRecognizer.state == UIGestureRecognizerState.ended || gestureRecognizer.state == UIGestureRecognizerState.cancelled {
                if progress > 0.5 {
                    self.interactivePopTransition.finish()
                    print("finished")
                } else {
                    self.interactivePopTransition.cancel()
                    print("canceled")
                }
                interactionInProgress = false
                self.interactivePopTransition = nil
            }
        }
  1. 手势开始后,我们初始化交互控制器self.interactivePopTransition,调整interactionInProgress的值并触发关闭视图控制器的操作。
  1. 手势进行时,我们不断调用update方法更新进度。它是UIPercentDrivenInteractiveTransition的一个方法,根据你传入的百分比值更新过渡动画。
  2. 如果手势被取消,更新interactionInProgress的值,并回滚过渡动画。
  3. 手势完成后,根据当前进度判断是取消还是完成过渡动画。

LSNavigationController中实现UINavigationControllerDelegate协议,

/// UINavigationControllerDelegate 以下两个协议均实现时,以第二个为准,
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if operation == UINavigationControllerOperation.push {
            return LSPushAnimation.init()
        }else if operation == UINavigationControllerOperation.pop {
            return LSPopAnimation.init()
        }
        return nil
    }
    
    /// 当返回值为nil时,上面的协议返回的push和pop动画才会有作用
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        if interactivePopTransition != nil {
            return interactivePopTransition
        }
        return nil
    }

这里的第一个方法前面已经用过,第二个是返回交互控制器,因为如果交互控制器不为空的话,就会调用控制器来控制交互,这样就使上面的push和pop失去的效果,所以只有在需要自定义交互控制器时才会返回,不然则返回nil即可(就像自定义滑动返回手势)。所以在上面的手势处理中才会在开始时初始化控制器,在结束后制为nil。

使用UIPercentDrivenInteractiveTransition的Demo

自定义交互控制器

在上面的demo基础上修改。

LSPercentDrivenInteractiveTransition需要自己实现UIViewControllerInteractiveTransitioning协议。
UIViewControllerInteractiveTransitioning协议总共有三个方法,其中startInteractiveTransition:是必须实现的方法,我们在里面初始化动画的状态:

///以下是自定义交互控制器 
///先初始化需要的变量
    var transitionContext : UIViewControllerContextTransitioning!
    var transitingView : UIView!
/// 以下----自定义交互控制器
    override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        self.transitionContext = transitionContext
        
        let containerView = transitionContext.containerView
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        
        containerView.insertSubview((toViewController?.view)!, belowSubview: (fromViewController?.view)!)
        
        self.transitingView = fromViewController?.view

    }
    override func update(_ percentComplete: CGFloat) {
        let scale = CGFloat(fabsf(Float(percentComplete - CGFloat(1.0))))
        transitingView?.transform = CGAffineTransform(scaleX: scale, y: scale)
        transitionContext?.updateInteractiveTransition(percentComplete)
    }
    
    
    
    func finishBy(cancelled: Bool) {
        if cancelled {
            UIView.animate(withDuration: 0.4, animations: {
                self.transitingView!.transform = CGAffineTransform(scaleX: 1, y: 1)
            }, completion: {completed in
                self.transitionContext!.cancelInteractiveTransition()
                self.transitionContext!.completeTransition(false)
            })
        } else {
            UIView.animate(withDuration: 0.4, animations: {
                print(self.transitingView)
                self.transitingView!.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
                print(self.transitingView)
            }, completion: {completed in
                self.transitionContext!.finishInteractiveTransition()
                self.transitionContext!.completeTransition(true)
            })
        }
    }

update:方法用来更新view的transform属性,finishBy:方法主要用来判断是进入下一个页面还是返回到之前的页面,并告知transitionContext目前的状态,以及对当前正在scale的view做最后的动画。这里的transitionContext和transitingView可以在前面的处理手势识别代码中取得,因此手势的处理中变成了:

///以下是自定义交互器
    func handleGesture(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
        var progress = gestureRecognizer.translation(in: gestureRecognizer.view?.superview).x / self.view.bounds.size.width
        progress = min(1.0, max(0.0, progress))
        //        let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview)
        //        var progress = Float(translation.x / 200)
        //        progress = fminf(fmaxf(progress, 0.0), 1.0)
        
        print("\(progress)")
        if gestureRecognizer.state == UIGestureRecognizerState.began {
            print("Began")
            self.interactivePopTransition = LSPercentDrivenInteractiveTransition()
            interactionInProgress = true
            self.popViewController(animated: true)
        } else if gestureRecognizer.state == UIGestureRecognizerState.changed {
            interactivePopTransition.update(progress)
            print("Changed")
        } else if gestureRecognizer.state == UIGestureRecognizerState.ended || gestureRecognizer.state == UIGestureRecognizerState.cancelled {
            interactivePopTransition.finishBy(cancelled: progress < 0.5)
            interactionInProgress = false
            self.interactivePopTransition = nil
        }
    }

这样就完成了自定义交互控制器的全部内容。

自定义控制器的demo

注意,视图控制器的这些同样适用于model视图的动画(连接中包括三个工程,其中一个是图片浏览,swift代码还不太熟悉,需要改善)。

推荐阅读更多精彩内容