自定义过渡

前言

对于Apple默认的转场动画(push、pop、modal)我们再熟悉不过了,但是这些显得单调的风格早已无法满足日益挑剔的用户了,对于极其重视用户体验的Apple肯定是不会让这个问题遗留下去的。早在iOS7,Apple就开放了视图控制器转场的API,到如今又迭代了三代iOS,相应的API已经很稳定了。如果是注意App体验的朋友一定发现了目前市面上的App导航控制器的转场绝大多数都是系统的默认实现,即经典的push、pop。它们最大的好处就是简单方便,对于功能性的App其实就已经够了,但是对于娱乐性的了?

娱乐性的App通常都具有良好的UI设计以及酷炫的视觉效果,这两者的合理融合是它们相互竞争的利器,同时也决定了用户的取向。像风靡全球的Angry Birds,笔者第一次接触这个游戏是那真是眼睛发亮,根本停不下来啊!所以为了今后的App能脱颖而出,就先定一个小目标——征服自定义过渡!

体会Apple的架构

UIPresentationController

UIPresentationController是iOS8新增加的类,以下是Apple官方文档对它的简介:

A UIPresentationController object provides advanced view and transition management for presented view controllers. From the time a view controller is presented until the time it is dismissed,UIKit uses a presentation controller to manage various aspects of the presentation process for that view controller. The presentation controller can add its own animations on top of those provided by animator objects,it can respond to size changes,and it can manage other aspects of how the view controller is presented onscreen.

大意为:

UIPresentationController对象对于呈现的视图控制器提供了高级的视图和过渡管理。从一个视图控制器呈现时到退场时,UIKit对于这个控制器使用了一个展示控制器来管理它各个方面的呈现过程。展示控制器可以添加那些由动画对象提供的自己的动画,它可以响应尺寸大小变化,并且它还可以管理视图控制器是如何呈现在屏幕上的其他方面的事情。

毫无疑问,从官方文档里面我们可以得到如何使用它的一切信息,但是谁又能说我们学习编程不是天生的“弱势群体”了。(ps:多希望我是混血儿啊!)笔者接下来的内容就不再引用官方文档,有需要的朋友请点Apple后面的官方文档。

对于系统默认的pop、push、modal相应的也有默认UIPresentationController对象行为默认的实现,当一个视图控制器被呈现时,UIKit就会调用视图控制器关联的UIPresentationController对象的presentationTransitionWillBegin()方法,当呈现阶段结束后就会调用其presentationTransitionDidEnd(_:)来告诉你过渡结束。对于文章将要给出的Demo在这里面的代码将是这样:

//When a view controller is about to be presented,UIKit calls the presentation controller’s presentationTransitionWillBegin() method.
    override func presentationTransitionWillBegin() {
        dismissView.frame = self.containerView!.bounds
        dismissView.alpha = 0.0

        self.containerView!.addSubview(dismissView)
        self.containerView!.addSubview(self.presentedView()!)

        let transitionCoordinator = self.presentedViewController.transitionCoordinator()!
        transitionCoordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) in
            self.dismissView.alpha = 1.0
            },completion: nil)
    }

//At the end of the presentation phase,UIKit calls the presentationTransitionDidEnd(_:) method to let you know that the transition finished.
    override func presentationTransitionDidEnd(completed: Bool) {
        if !completed {
            dismissView.removeFromSuperview()
        }
    }

当视图控制器退场时,也会调用UIPresentation对象的dismissalTransitionWillBegin()dismissalTransitionDidEnd(_:)方法,就像这样:

override func dismissalTransitionWillBegin() {
        let transitionCoordinator = self.presentedViewController.transitionCoordinator()!
        transitionCoordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) in
            self.dismissView.alpha = 0.0
            },completion: nil)
    }

    override func dismissalTransitionDidEnd(completed: Bool) {
        if completed {
            dismissView.removeFromSuperview()
        }
    }

同时视图控制器被呈现的 view 在过渡动画结束后的最终位置也是由 UIPresentationViewController 来负责给定的,我们只需要重载frameOfPresentedViewInContainerView()就可以了:

override func frameOfPresentedViewInContainerView() -> CGRect {
        var frame = self.containerView!.bounds
        frame = CGRectInset(frame,50.0,200.0)

        return frame
    }

如果你还想让界面的尺寸能适应屏幕的旋转,还需要在viewWillTransitionToSize(size:coordinator:)方法里做出一些改变:

override func viewWillTransitionToSize(size: CGSize,withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransitionToSize(size,withTransitionCoordinator: coordinator)

        coordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) in
            self.dismissView.frame = self.containerView!.bounds
            },completion: nil)
    }

(ps:笔者忽略一个细节——整个界面是在storyboard里搭建的,加上用了所有竖屏的sizeClass限定,导致旋转之后就只有transition后的半透明的黑色遮罩能适应屏幕的旋转,其他的都“不翼而飞”,真是抱歉!)

UIViewControllerAnimatedTransitioning

就像我们看到的这样,它不是以明显的OC类名或者部分类名结尾的API。正如各位想得这样,它只是一个协议,一个负责动画过渡的协议。而且它的协议方法也只有两个:

  • transitionDuration(_:):负责返回动画的过渡时间
  • animateTransition(_:):负责动画过渡的具体行为

前者相当直观,笔者不再赘述。后者会传回一个参数——过渡上下文(transitionContext),通过对应的键我们可以访问实现过渡所必须的一些对象:

  • viewControllerForKey:访问过渡前后的视图控制器
  • containerView:访问过渡当前控制器的视图(在哪个控制器里访问就获得哪个控制器的视图)
  • viewForKey:访问过渡前后控制器的视图(与前者的不同在于可以在一个控制器中访问其过渡前后的视图)
  • finalFrameForViewController、initialFrameForViewController:获取过渡前后控制器过渡开始和结束的frame

UIViewControllerTransitioningDelegate

如果你想掌控自己的视图控制器的过渡方式,那么就应该让控制器遵守标题所指的协议,并重新设置它的过渡代理。文章的Demo中将控制器的模态视图的风格设置为自定义,因为Demo展示的正是模态视图的自定义过渡,否则将会和系统的风格冲突,而这将为你展现iOS的“凌乱美”。Demo中使用了如下方法:

  • presentationControllerForPresentedViewController(_:presentingViewController:sourceViewController:):iOS8新增方法,给即将要被呈现的视图控制器自身指定UIPresentationController对象,来管理过渡前后的视图切换
  • animationControllerForPresentedController(_:presentingViewController:sourceViewController:):给即将要被呈现的视图控制器自身指定过渡的动画
  • animationControllerForDismissedController(_:):给即将退场的视图控制器自身指定过渡动画

下面通过几张图片(来源)来说明presentedViewController、presentingViewController:

可以和用户交互的ViewController叫做presentedViewController,也就是过渡的目的视图控制器,在Demo中也就是MessageViewController。presentingViewController就是后面被部分遮盖的UIViewController,即Demo中的ViewController。presentedViewController就是要呈现(presentation)的content,所有的UIViewController的 呈 现 从iOS8开始由UIPresentationController来管理,它定义了content和chrome的动画,chrome可以理解为presentedViewController与presentingViewController的隔离层,即Demo中的半透明的黑色遮罩:

你可能还注意到了sourceController,它其实就是presentingController,可以将它理解为触发过渡的源控制器,Demo中得到了验证:

func presentationControllerForPresentedViewController(presented: UIViewController,presentingViewController presenting: UIViewController,sourceViewController source: UIViewController) -> UIPresentationController? {
        if presented == self {
            //presented: MessageViewController source: ViewController
            print("presented: \(presented),source:\(source)")

            return CustomPresentationController(presentedViewController: presented,presentingViewController: presenting)
        } else {
            return nil
        }
    }

    func animationControllerForPresentedController(presented: UIViewController,presentingController presenting: UIViewController,sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if source == presenting {
            print("equal")
        }
        if presented == self {
            print("presented: \(presented)),presenting: \(presenting) source:\(source)")

            return CustomAnimationController(isPresenting: true)
        } else {
            return nil
        }

    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if dismissed == self {
            //dismissed: MessageViewController
            print("dismissed: \(dismissed)")

            return CustomAnimationController(isPresenting: false)
        } else {
            return nil
        }
    }

最终效果图:

项目源代码在这里,由于SwiftAPI的不稳定,原作者的Demo已经不能正常运行,为此笔者做了新的调整以便正常运行。同时这里有篇扩展资料,是关于自定义导航过渡的文章(OC实现),并讲解了如何构建可交互性的过渡,文章的Demo也很有使用价值,值得花时间去品读!

总结

“告诉我过渡前后的动画方式和所要展示的内容,我就按照你的要求完成过渡”,感谢Apple的UIKit团队所作出的努力,让我们能以清晰的思路和优雅的代码架构完成我们所想要的过渡效果。细想一下,凡此种种,不就是良性App架构的体现吗?

参考:
http://nonomori.farbox.com/post/ios-8-presentation-controller

写在后面的扩展资料:

唐巧的技术博客:iOS视图控制器转场详解

引用其中的一句话:

转场动画的本质是对即将消失的当前视图和即将出现的下一视图进行动画。

相关文章

软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘...
现实生活中,我们听到的声音都是时间连续的,我们称为这种信...
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿...
【Android App】实战项目之仿抖音的短视频分享App(附源码和...
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至...
因为我既对接过session、cookie,也对接过JWT,今年因为工作...