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?) -> TimeIntervalfunc 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
// done
})
}
}
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
// done
})

动画部分

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
// some property
}
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: {
// animation
}, completion: { _ in
transitionContext.completeTransition(true)
})
}
}

理想的效果如下图所示