0x00 前戏
半年前我写过一篇模仿花瓣app转场动画的文章。
当时的情况是我使用了 UINavigationController,用了一种不太正常的做法实现了效果。最近一直在看 Raywenderlich 的动画书,恰好看到了一样的动画效果,不过书中的是基于 func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?)
实现。书中也有基于 UINavigationController 的自定义转场动画,实现的协议略有不同,但方法都是很接近的。
这篇文章和之前的文章实现了一样的效果,不过使用的是不同的方法。
0x01 自定义转场
自定义转场动画需要给将要显示的ViewController设置transitioningDelegate
属性,这个delegate需要实现UIViewControllerTransitioningDelegate
协议,并实现两个方法:func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
和func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
,从方法名可以看出,前者用来处理转入的动画,后者用来处理转出的动画。这两个方法都返回一个UIViewControllerAnimatedTransitioning?
,即一个实现了UIViewControllerAnimatedTransitioning
协议的对象的实例,或者返回nil。
然后新建一个类,实现UIViewControllerAnimatedTransitioning
协议,实现该协议需要实现两个方法:func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
和func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
,前者返回该转场动画的持续时间。后者方法内部可以放置动画部分的相关代码,提供了一个参数transitionContext
,该参数有一个属性containerView
提供了转场过程的中间容器视图,该参数还提供了一些方法让你获取到与本次转场关联的两个View以及它们的ViewController。转场的过程如下图所示
所有的动画应当在containerView
中执行。在动画结束后记得调用transitionContext.completeTransition(true)
通知系统转场结束。
0x02 Talk is cheap, show me the code
下面是核心代码
转场部分(present)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class GameListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let growthTransition = GrowthTransition() func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: storyboardGameViewController) as! GameViewController vc.transitioningDelegate = self present(vc, animated: true, completion: { _ in }) } } extension GameListViewController: UIViewControllerTransitioningDelegate { func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { growthTransition.presenting = true return growthTransition } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { growthTransition.presenting = false return growthTransition } }
|
转场部分(dismiss)
1 2 3
| presentingViewController?.dismiss(animated: true, completion: { _ in })
|
动画部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class GrowthTransition: NSObject { let duration = 1.0 } extension GrowthTransition: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView let gameVC = presenting ? transitionContext.viewController(forKey: .to)! as! GameViewController : transitionContext.viewController(forKey: .from) as! GameViewController let origGameView = presenting ? transitionContext.view(forKey: .to)! : transitionContext.view(forKey: .from)! containerView.addSubview(origGameView) containerView.bringSubview(toFront: origGameView) UIView.animate(withDuration: duration, delay: 0.0, options: [], animations: { }, completion: { _ in transitionContext.completeTransition(true) }) } }
|
理想的效果如下图所示