Skip to content

View Controller Transition PartII

seedante edited this page Jul 11, 2016 · 7 revisions

本文为全篇第二部分。

全篇目录

第一部分:以下目录无法跳转,请点击该链接查看内容

本部分的目录(可跳转):

第三部分:以下目录无法跳转,请点击该链接查看内容

阶段二:交互式转场

激动人心的部分来了,好消息是交互转场的实现难度比你想象的要低。

实现交互化

在非交互转场的基础上将之交互化需要两个条件:

  1. 由转场代理提供交互控制器,这是一个遵守<UIViewControllerInteractiveTransitioning>协议的对象,不过系统已经打包好了现成的类UIPercentDrivenInteractiveTransition供我们使用。我们不需要做任何配置,仅仅在转场代理的相应方法中提供一个该类实例便能工作。另外交互控制器必须有动画控制器才能工作。

  2. 交互控制器还需要交互手段的配合,最常见的是使用手势,或是其他事件,来驱动整个转场进程。

满足以上两个条件很简单,但是很容易犯错误。

正确地提供交互控制器

如果在转场代理中提供了交互控制器,而转场发生时并没有方法来驱动转场进程(比如手势),转场过程将一直处于开始阶段无法结束,应用界面也会失去响应:在 NavigationController 中点击 NavigationBar 也能实现 pop 返回操作,但此时没有了交互手段的支持,转场过程卡壳;在 TabBarController 的代理里提供交互控制器存在同样的问题,点击 TabBar 切换页面时也没有实现交互控制。因此仅在确实处于交互状态时才提供交互控制器,可以使用一个变量来标记交互状态,该变量由交互手势来更新状态。

以为 NavigationController 提供交互控制器为例:

class SDENavigationDelegate: NSObject, UINavigationControllerDelegate {
    var interactive = false
    let interactionController = UIPercentDrivenInteractiveTransition()
    ...
    
    func navigationController(navigationController: UINavigationController,
       interactionControllerForAnimationController
                               animationController: UIViewControllerAnimatedTransitioning) 
                                                   -> UIViewControllerInteractiveTransitioning? {
        return interactive ? interactionController : nil
    }
}

TabBarController 的实现类似,Modal 转场代理分别为 presentation 和 dismissal 提供了各自的交互控制器,也需要注意上面的问题。

问题的根源是交互控制的工作机制导致的,交互过程实际上是由转场环境对象<UIViewControllerContextTransitioning>来管理的,它提供了如下几个方法来控制转场的进度:

func updateInteractiveTransition(_ percentComplete: CGFloat)//更新转场进度,进度数值范围为0.0~1.0。
func cancelInteractiveTransition()//取消转场,转场动画从当前状态返回至转场发生前的状态。
func finishInteractiveTransition()//完成转场,转场动画从当前状态继续直至结束。

交互控制协议<UIViewControllerInteractiveTransitioning>只有一个必须实现的方法:

func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning)

在转场代理里提供了交互控制器后,转场开始时,该方法自动被 UIKit 调用对转场环境进行配置。

系统打包好的UIPercentDrivenInteractiveTransition中的控制转场进度的方法与转场环境对象提供的三个方法同名,实际上只是前者调用了后者的方法而已。系统以一种解耦的方式使得动画控制器,交互控制器,转场环境对象互相协作,我们只需要使用UIPercentDrivenInteractiveTransition的三个同名方法来控制进度就够了。如果你要实现自己的交互控制器,而不是UIPercentDrivenInteractiveTransition的子类,就需要调用转场环境的三个方法来控制进度,压轴环节我们将示范如何做。

交互控制器控制转场的过程就像将动画控制器实现的动画制作成一部视频,我们使用手势或是其他方法来控制转场动画的播放,可以前进,后退,继续或者停止。finishInteractiveTransition()方法被调用后,转场动画从当前的状态将继续进行直到动画结束,转场完成;cancelInteractiveTransition()被调用后,转场动画从当前的状态回拨到初始状态,转场取消。

在 NavigationController 中点击 NavigationBar 的 backBarButtomItem 执行 pop 操作时,由于我们无法介入 backBarButtomItem 的内部流程,就失去控制进度的手段,于是转场过程只有一个开始,永远不会结束。其实我们只需要有能够执行上述几个方法的手段就可以对转场动画进行控制,用户与屏幕的交互手段里,手势是实现这个控制过程的天然手段,我猜这是其被称为交互控制器的原因。

交互手段的配合

下面使用演示如何利用屏幕边缘滑动手势UIScreenEdgePanGestureRecognizer在 NavigationController 中控制 Slide 动画控制器提供的动画来实现右滑返回的效果,该手势绑定的动作方法如下:

func handleEdgePanGesture(gesture: UIScreenEdgePanGestureRecognizer){
    //根据移动距离计算交互过程的进度。
    let percent = ...
    let interactionController = self.navigationDelegate?.interactionController
    switch gesture.state{
    case .Began:
        //转场开始前获取代理,一旦转场开始,VC 将脱离控制器栈,此后 self.navigationController 返回的是 nil。
        self.navigationDelegate = self.navigationController?.delegate as? SDENavigationDelegate
        //更新交互状态
        self.navigationDelegate?.interactive = true
        //1.如果转场代理提供了交互控制器,它将从这时候开始接管转场过程。
        self.navigationController?.popViewControllerAnimated(true)
    case .Changed:
        //2.更新进度:
        interactionController?.updateInteractiveTransition(percent)
    case .Cancelled, .Ended:
        //3.结束转场:
        if percent > 0.5{
            //完成转场。
            interactionController?.finishInteractiveTransition()
        }else{
            //或者,取消转场。
            interactionController?.cancelInteractiveTransition()
        }
        //无论转场的结果如何,恢复为非交互状态。
        self.navigationDelegate?.interactive = false
    default: self.navigationDelegate?.interactive = false
    }
}

交互转场的流程就是三处数字标记的代码。不管是什么交互方式,使用什么转场方式,都是在使用这三个方法控制转场的进度。**对于交互式转场,交互手段只是表现形式,本质是驱动转场进程。**很希望能够看到更新颖的交互手法,比如通过点击页面不同区域来控制一套复杂的流程动画。

TabBarController 的 Demo 中也实现了滑动切换 Tab 页面,代码是类似的,就不占篇幅了;示范的 Modal 转场我没有为之实现交互控制,原因也提到过了,没有比较合乎操作直觉的交互手段,不过真要为其添加交互控制,代码和上面是类似的。[修正]由于我没有为本文 Modal 转场的示例实现交互控制,而且没有对 presentingView 添加动画,因此漏掉了一个大坑。这个坑就是在 Custom 模式下交互控制无法控制 presentingView 上的动画,感谢简书用户@1269 发现并找到了解决办法。此大坑请看特殊的 Modal 转场交互化

到此为止,一个完整的交互转场动画就完成了,在转场代理中提供一个UIPercentDrivenInteractiveTransition实例对象外加实现手势的响应办法就够了,这里不涉及任何动画。

转场交互化后结果有两种:完成和取消。取消后动画将会原路返回到初始状态,但已经变化了的数据怎么恢复?

一种情况是,控制器的系统属性,比如,在 TabBarController 里使用上面的方法实现滑动切换 Tab 页面,中途取消的话,已经变化的selectedIndex属性该怎么恢复为原值;上面的代码里,取消转场的代码执行后,self.navigationController返回的依然还是是 nil,怎么让控制器回到 NavigationController 的控制器栈顶。对于这种情况,UIKit 自动替我们恢复了,不需要我们操心(可能你都没有意识到这回事);

另外一种就是,转场发生的过程中,你可能想实现某些效果,一般是在下面的事件中执行,转场中途取消的话可能需要取消这些效果。

func viewWillAppear(_ animated: Bool)
func viewDidAppear(_ animated: Bool)
func viewWillDisappear(_ animated: Bool)
func viewDidDisappear(_ animated: Bool)

交互转场介入后,视图在这些状态间的转换变得复杂,WWDC 上苹果的工程师还表示转场过程中 view 的Will系方法和Did系方法的执行顺序并不能得到保证,虽然几率很小,但如果你依赖于这些方法执行的顺序的话就可能需要注意这点。而且,Did系方法调用时并不意味着转场过程真的结束了。另外,fromView 和 toView 之间的这几种方法的相对顺序更加混乱,具体的案例可以参考这里:The Inconsistent Order of View Transition Events

如何在转场过程中的任意阶段中断时取消不需要的效果?这时候该转场协调器(Transition Coordinator)再次出场了。

Transition Coordinator

转场协调器(Transition Coordinator)的出场机会不多,但却是关键先生。Modal 转场中,UIPresentationController类只能通过转场协调器来与动画控制器同步,并行执行其他动画;这里它可以在交互式转场结束时执行一个闭包:

func notifyWhenInteractionEndsUsingBlock(_ handler: (UIViewControllerTransitionCoordinatorContext) -> Void)

当转场由交互状态转变为非交互状态(在手势交互过程中则为手势结束时),无论转场的结果是完成还是被取消,该方法都会被调用;得益于闭包,转场协调器可以在转场过程中的任意阶段搜集动作并在交互中止后执行。闭包中的参数是一个遵守<UIViewControllerTransitionCoordinatorContext>协议的对象,该对象由 UIKit 提供,和前面的转场环境对象<UIViewControllerContextTransitioning>作用类似,它提供了交互转场的状态信息。

override func viewWillAppear(animated: Bool) {
    super.viewWillDisappear(animated)
    self.doSomeSideEffectsAssumingViewDidAppearIsGoingToBeCalled()
    //只在处于交互转场过程中才可能取消效果。
    if let coordinator = self.transitionCoordinator() where coordinator.initiallyInteractive() == true{
        coordinator.notifyWhenInteractionEndsUsingBlock({
            interactionContext in
            if interactionContext.isCancelled(){
                self.undoSideEffects()
            }
        })
    }
}

不过交互状态结束时并非转场过程的终点(此后动画控制器提供的转场动画根据交互结束时的状态继续或是返回到初始状态),而是由动画控制器来结束这一切:

optional func animationEnded(_ transitionCompleted: Bool)

如果实现了该方法,将在转场动画结束后调用。@liwenDeng 发现这个方法在 UITabBarController 的转场结束后被调用了两次,检查函数调用帧栈后猜测是 UIKit 的内部实现问题,尚无解决办法。

UIViewController 可以通过transitionCoordinator()获取转场协调器,该方法的文档中说只有在 Modal 转场过程中,该方法才返回一个与当前转场相关的有效对象。实际上,NavigationController 的转场中 fromVC 和 toVC 也能返回一个有效对象,TabBarController 有点特殊,fromVC 和 toVC 在转场中返回的是 nil,但是作为容器的 TabBarController 可以使用该方法返回一个有效对象。

转场协调器除了上面的两种关键作用外,也在 iOS 8 中的适应性布局中担任重要角色,可以查看<UIContentContainer>协议中的方法,其中响应尺寸和屏幕旋转事件的方法都包含一个转场协调器对象,视图的这种变化也被系统视为广义上的 transition,参数中的转场协调器也由 UIKit 提供。这个话题有点超出本文的范围,就不深入了,有需要的话可以查看文档和相关 session。

向非交互阶段的平滑过渡

最近研究交互动画才发现我遗漏了这个部分。假如你在屏幕上用手指移动一个视图,当你放手后,你希望视图应该以你放手的速度继续下去,这样看起来才自然。交互结束后,应该让剩余的转场动画以手指离开的速度继续。

UIViewControllerInteractiveTransitioning协议定义了两个属性用于这种情况:

completionCurve //交互结束后剩余动画的速率曲线
completionSpeed //交互结束后动画的开始速率由该参数与原来的速率相乘得到,实际上是个缩放参数

上面处理 pop 转场动画的方法里,应该在手势结束时这样处理:

case .Cancelled, .Ended:
    interactionController.completionCurve = .EaseOut
    let speed = abs(gesture.velocityInView(view).x)
    //如果进度超过一半,完成这次转场
    if percent > 0.5{
        interactionController.completionSpeed = speed / ((1 - percent) * view.frame.width)
        interactionController.finishInteractiveTransition()
    }else{//取消这次转场
        interactionController.completionSpeed = speed / (percent * view.frame.width)
        interactionController.cancelInteractiveTransition()
    }

这里应该使用单位变化速率(即你要的速率/距离)。注意:completionSpeed会影响剩余的动画时间,而不是之前设定的转场动画时间剩下的时间;当completionSpeed很小时剩余的动画时间可能会被拉伸得很长,所以过滤下较低的速率比较好。如果不设置两个参数,转场动画将以原来的速率曲线在当前进度的速率继续。不过从实际使用效果来看,往往不到0.5s的动画时间,基本上看不出什么效果来。

特殊的 Modal 转场交互化

Modal 转场真是麻烦啊。此坑的具体表现是 Custom 模式下交互控制时无法控制 presentingView 上添加的动画。至于原因,首先你得知道是交互控制的机制,本来实现转场也不需要了解这方面的知识,但是有此坑,不得不讲一下,交互控制的关键在于 CALayer 和CAMediaTiming 协议,如果你有兴趣,可以阅读自定义容器控制器转场这个章节。这种控制视图动画进度的手段适用于视图及其子视图,这样为转场实现交互化的时候只需要控制 containerView 即可,而从 Modal 转场的差异可以知道,presentingView 并非 containerView 的子视图,两者是同层次的视图。因此 Modal 转场交互化无法控制 presentingView 上的动画,等等,FullScreen 模式下没有问题呀,细说的话,FullScreen 模式下 presentation 转场与 Custom 模式有着同样的困境,FullScreen 模式在 dismissal 转场下则不存在这个问题,想想为什么。对此,我的猜测是 FullScreen 模式下交互控制针对的是 presentingView 和 containerView 的父系视图或者对两者同时进行了交互控制,从解决手法看后者的可能性大一些。在 Custom 模式下,UIKit 又对 presengtingView 撒手不管了,怎么办?

感谢简书用户@1269 找到了解决的办法,具体可参考此处简书评论。以下是解决办法:

在 iOS 8 以上的系统里,转场时通过提供UIPresentationController类并重写以下方法并返回true可以解决上述问题:

func shouldRemovePresentersView() -> Bool

UIPresentationController类的作用可参考前面 iOS 8 的改进:UIPresentationController 一节。注意,UIPresentationController参与转场并没有改变 presentingView 与 containerView 的层次关系,能够修复这个问题我猜测是重写的该方法返回true后交互转场控制同时对这两个视图进行了控制而非对两者的父系视图进行控制,因为这个方法返回false时不起作用。

那 iOS 8 以下的系统怎么办?最好的办法是转场时不要对 presentingView 添加动画,不是开玩笑,我觉得 Modal 转场的视觉风格在 presentingView 上添加动画没有什么必要,不过,真要这样做还是得解决不是。在 Modal 转场的差异里我尝试了在 Custom 模式来下模拟 FullScreen 模式,就是在动画控制器里用变量维护 presentingView 的父视图,剩下的部分和通用的动画控制器没有区别,将 presentingView 加入到 containerView,只是在转场结束后将 presentingView 恢复到原来的视图结构里。这样,交互控制就能控制 presentingView 上的动画了。如果你要在 Custom 模式下第三方的动画控制器,这些动画控制器都需要调整,代价不小。

Modal 转场 Custom 模式下用于交互化的的动画控制器:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {            
    ...
    
    //处理 Presentation 转场:
    if toVC.isBeingPresented(){
        //这个非常关键,而由谁来维持这个父视图呢,看看动画控制器以及转场代理的关系就知道这是个很麻烦的事情。
        presentingSuperview = fromView.superview
        //1:将 presentingView 加入到 containerView 下,这样 presentation 转场时也能控制 presentingView 上的动画
        fromView.removeFromSuperview()
        containerView.addSubview(fromView)
        containerView.addSubview(toView)               
        
        UIView.animateWithDuration(duration, delay: 0, options: .CurveEaseInOut, animations: {
                  /*动画代码*/
            }, completion: {_ in
                //2:照旧
                let isCancelled = transitionContext.transitionWasCancelled()
                transitionContext.completeTransition(!isCancelled)
        })
    }
    //处理 Dismissal 转场:
    if fromVC.isBeingDismissed(){
        //如果在 presentation 转场里已经将 presentingView 添加到 containerView 里了,这里没必要再加一次了。
        UIView.animateWithDuration(duration, animations: {
            /*动画代码*/
            }, completion: { _ in
                //2:照旧
                let isCancelled = transitionContext.transitionWasCancelled()
                transitionContext.completeTransition(!isCancelled)
                
                //最后一步:恢复 presentingView 到原来的视图结构里。在 FullScreen 模式下,UIKit 会自动做这件事,可以省去这一步。
                toView.removeFromSuperview()
                presentingSuperview.addSubview(toView)
        })
    }
}

封装交互控制器

UIPercentDrivenInteractiveTransition类是一个系统提供的交互控制器,在转场代理的相关方法里提供一个该类实例就够了,还有其他需求的话可以实现其子类来完成,那这里的封装是指什么?系统把交互控制器打包好了,但是交互控制器工作还需要其他的配置。程序员向来很懒,能够自动完成的事绝不肯写一行代码,写一行代码就能搞定的事绝不写第二行,所谓少写一行是一行。能不能顺便把交互控制器的配置也打包好省得写代码啊?当然可以。

热门转场动画库 VCTransitionsLibrary 封装好了多种动画效果,并且自动支持 pop, dismissal 和 tab change 等操作的手势交互,其手法是在转场代理里为 toVC 添加手势并绑定相应的处理方法。

为何没有支持 push 和 presentation 这两种转场?因为 push 和 presentation 这两种转场需要提供 toVC,而库并没有 toVC 的信息,这需要作为使用者的开发者来提供;对于逆操作的 pop 和 dismiss,toVC 的信息已经存在了,所以能够实现自动支持。而 TabBarController 则是个例外,它是在已知的子 VC 之间切换,不存在这个问题。需要注意的是,库这样封装了交互控制器后,那么你将无法再让同一种手势支持 push 或 presentation,要么只支持单向的转场,要么你自己实现双向的转场。当然,如果知道 toVC 是什么类的话,你可以改写这个库让 push 和 present 得到支持。不过,对于在初始化时需要配置额外信息的类,这种简单的封装可能不起作用。VCTransitionsLibrary 库还支持添加自定义的简化版的动画控制器和交互控制器,在封装和灵活之间的平衡控制得很好,代码非常值得学习。

只要愿意,我们还可以变得更懒,不,是效率更高。FDFullscreenPopGesture 通过 category 的方法让所有的 UINavigationController 都支持右滑返回,而且,一行代码都不用写,这是配套的博客:一个丝滑的全屏滑动返回手势。那么也可以实现一个类似的 FullScreenTabScrollGesture 让所有的 UITabBarController 都支持滑动切换,不过,UITabBar 上的 icon 渐变动画有点麻烦,因为其中的 UITabBarItem 并非 UIView 子类,无法进行动画。WXTabBarController 这个项目完整地实现了微信界面的滑动交互以及 TabBar 的渐变动画。不过,它的滑动交互并不是使用转场的方式完成的,而是使用 UIScrollView,好处是兼容性更好。兼容性这方面国内的环境比较差,iOS 9 都出来了,可能还需要兼容 iOS 6,而自定义转场需要至少 iOS 7 的系统。该项目实现的 TabBar 渐变动画是基于 TabBar 的内部结构实时更新相关视图的 alpha 值来实现的(不是UIView 动画),这点非常难得,而且使用 UIScrollView 还可以实现自动控制 TabBar 渐变动画,相比之下,使用转场的方式来实现这个效果会麻烦一点。

一个较好的转场方式需要顾及更多方面的细节,NavigationController 的 NavigationBar 和 TabBarController 的 TabBar 这两者在先天上有着诸多不足需要花费更多的精力去完善,本文就不在这方面深入了,上面提及的几个开源项目都做得比较好,推荐学习。

交互转场的限制

如果希望转场中的动画能完美地被交互控制,必须满足2个隐性条件:

  1. 使用 UIView 动画的 API。你当然也可以使用 Core Animation 来实现动画,甚至,这种动画可以被交互控制,但是当交互中止时,会出现一些意外情况:如果你正确地用 Core Animation 的方式复现了 UIView 动画的效果(不仅仅是动画,还包括动画结束后的处理),那么手势结束后,动画将直接跳转到最终状态;而更多的一种状况是,你并没有正确地复现 UIView 动画的效果,手势结束后动画会停留在手势中止时的状态,界面失去响应。使用 Core Animation 实现完美的交互控制也是可以的,详见压轴环节的自定义容器控制器转场,只不过你需要处理很多细节问题,而 UIView 动画 API 作为对 Core Animation 的高级封装,替我们省去了不少麻烦的细节,显著降低了交互转场动画的实现成本,这大概就是官方 Session 里提到必须使用 UIView 动画 API 的原因。
  2. 在动画控制器的animateTransition:中提交动画。问题和第1点类似,在viewWillDisappear:这样的方法中提交的动画也能被交互控制,但交互停止时,立即跳转到最终状态。

如果你希望制作多阶段动画,在某个动画结束后再执行另外一段动画,可以通过 UIView Block Animation 的 completion 闭包来实现动画链,或者是通过设定动画执行的延迟时间使得不同动画错分开来,但是交互转场不支持这两种形式。UIView 的 Keyframe Animation API 可以帮助你,通过在动画过程的不同时间节点添加关键帧动画就可以实现多阶段动画。我实现过一个这样的多阶段转场动画,Demo 在此:CollectionViewAlbumTransition

iOS 10 新特性:全程交互控制

在转场动画里,非交互转场与交互转场之间有着明显的界限:如果以交互转场开始,尽管在交互结束后会切换到动画过程,但之后无法再次切换到交互过程,只能等待其结束;如果以非交互转场开始,在动画结束前是无法切换到交互过程的,只能等待其结束。上月的WWDC 2016 Session 216: Advances in UIKit Animations and Transitions 介绍了 iOS 10 全新的动画 API,并将其引入了转场协议中,这使得非交互转场与交互转场之间的界限不再泾渭分明。

让转场动画在非交互状态与交互状态之间自由切换很困难,UIViewPropertyAnimator 类实现了需要的所有基础功能,使得难度降低了许多。在 session 的现场演示中,工程师大致展示了如何使用该类从头打造可全程在非交互与交互状态间自由切换的转场动画。实际上转场协议由于为了实现高度定制化,定义的方法是比较冗余的,iOS 10 对此基础上引入的新 API 使得略显臃肿的转场协议更加复杂,虽然在演示中添加的代码只有百来行,另一方面演示的转场动画本身也相对复杂,使得这一切看上去很非常复杂。转场协议引入的新 API 很繁琐,我就不贴出来了,在该 session 和转场协议文档可以看到相关的变化。

Session 216 中工程师为了展示转场协议的新增部分,特意实现了这些部分,事实上,如果你对UIViewPropertyAnimator类足够熟悉,在实现转场动画在非交互与交互状态之间自由切换的基础上,还可以大幅精简现有的转场协议体系:我们只需要实现转场代理以及动画控制,以及提供一个UIViewPropertyAnimator对象,就可以实现转场动画的全程交互控制,甚至不需交互控制器,总的代码量几乎没有增长。

下面展示使用UIViewPropertyAnimator来实现 Push 和 Pop 过程的全程交互控制:不管转场动画是以交互转场还是非交互转场开始,你可以在转场动画结束前的任何时间,随意在非交互状态与交互状态之间切换。

实现转场代理并提供动画控制器:

@available(iOS 10.0, *)
class SDENavigationDelegate: NSObject, UINavigationControllerDelegate, UIViewControllerAnimatedTransitioning {
   
    // MARK: UINavigationControllerDelegate
    func navigationController(_ navigationController: UINavigationController, 
                    animationControllerFor operation: UINavigationControllerOperation, 
                                         from fromVC: UIViewController, 
                                             to toVC: UIViewController) 
                                                     -> UIViewControllerAnimatedTransitioning? {
        self.operation = operation
        return self
    }

    // 提供一个 UIViewPropertyAnimator,由它来执行转场动画以及实现交互控制
    var transitionAnimator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: 1, curve: .easeOut, animations: nil)
        
     // MARK: UIViewControllerAnimatedTransitioning
    func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(_ transitionContext: UIViewControllerContextTransitioning){
        /*从 transitionContext 获取参与转场的视图*/        
        transitionAnimator.addAnimations({
            /*简单的位移动画*/
        })
    
        transitionAnimator.addCompletion({ position in
            let completed = position == .end ? true : false //根据动画结束的位置判断转场动画已经完成还是取消了
            transitionContext.completeTransition(completed)
        })
        transitionAnimator.startAnimation() //UIViewPropertyAnimator 需要手动启动
    }
 }

在转场动画结束前的任何时刻点击视图让动画反向:

func handleTap(gesture: UITapGestureRecognizer){
    let animator = navigationDelegate.transitionAnimator //获取转场代理中的 animator
    switch gesture.state {
    case .ended, .cancelled:
        switch animator.state {
        case .active:
            //逆转转场动画
            animator.pauseAnimation() //暂停动画
            animator.isReversed = !(animator.isReversed) //置反动画方向
            animator.startAnimation() //继续动画
        default:break
        }
    default:break
    }
}

在转场动画结束前的任何时刻滑动视图来控制转场过程:

func handlePan(gesture: UIPanGestureRecognizer){
    let animator = navigationDelegate.transitionAnimator
    switch gesture.state{
    case .began:
        //没有转场发生,启动转场,此次转场以交互状态开始。
        if animator.isRunning == false{
			/*执行 push 或 pop 操作*/
        }else{//有转场正在进行,暂停动画。
            animator.pauseAnimation()
       }
    case .changed:
        if animator.isRunning{//此次转场以交互状态开始,先暂停
            animator.pauseAnimation() 
        }else if animator.state == .active{//根据手势位置更新转场动画进度
            animator.fractionComplete = ...
        }
    case .cancelled, .ended:
        if animator.state == .active{
            if animator.fractionComplete < 0.3{
                animator.isReversed = true //根据当前进度判断是否取消转场:让动画返回起始位置
            }
            //以手指离开屏幕的速度继续执行剩下的转场动画,保证动画的流畅
            animator.continueAnimation(withTimingParameters: UISpringTimingParameters(dampingRatio: 0.9, initialVelocity: initialVelocity), durationFactor: 0)
        }
    default: break
    }
}

Demo 地址:iOS10PushPop

转场协议的枢纽 UIViewControllerContextTransitioning 定义了实现交互控制的核心方法,在系统支持的转场里,遵守该协议的对象由系统提供,所以实现交互转场非常得容易。在 iOS 10 里这个协议添加了pauseInteractiveTransition(),这个方法是在非交互与交互状态之间自由切换时缺失的一环。我在自定义容器控制器转场中实现了交互控制,但要在非交互与交互状态之间自由切换,目前还没有稳妥的解决办法:Core Animation 里构建交互动画的 API 虽然有文档解释,其运作机制基本上和一个黑盒没有区别,目前还没找到实现将动画自由改变方向的相关属性,也许没有公开也说不定,这种情况下,不得不依赖CADisplayLink。而且利用这些黑盒接口实现的交互动画出了 Bug 也只能瞎摸索,这时候借助 Facebook Pop 这类大厂的第三方动画库可能会更好一点。有了UIViewPropertyAnimator后,这些困难不再是困扰,实现自定义容器控制器的全程交互控制将简单得多,有空的话(基本不用指望)实现下。

本文第一部分:转场机制、非交互转场

本文第三部分:案例分析、自定义容器控制器转场

版权申明以及其他

版权申明:我已将本文在微信公众平台的发表权「独家代理」给 iOS 开发(iOSDevTips)微信公共帐号。扫码关注「iOS 开发」:

二维码

关于 View Controller Transition 这个主题,我其实不愿意使用转场这个翻译,不过使用字面翻译控制器转换也不方便,而且由于 objccn.io 的影响,大家都在用这个翻译,为了统一我还是使用了这个翻译,还有 context,每次遇到这个词,大家都是翻译成上下文,我个人不喜欢这个翻译。老实说,我觉得大家交流技术时对于专业词汇还是尽量用英文表达,不容易出错。另外关于这个话题的延伸阅读,我还是推荐前言里提到的官方文档:View Controller Programming Guide for iOS

如心血来潮,请随意赞赏^_^。但是要在支付宝里加我好友,这让我很困扰,支付宝好友还是算了。希望你是出于文章写得好或者帮到你的原因给我打赏,而非其他,因为打赏要加我好友还是算了。有问题,在我的简书的本文章页面下留言就好,这是最有效的交流方式。

AlipayDonate