自定义转场这个技术相信大家都用过或者听说过,不过如果你经常使用自定义转场或者自定义转场动画做得比较复杂或许会发现AnimatedTransitioning与目标控制器的交互并没有那么友好。本文旨在提供一个新思路,减少AnimatedTransitioning与目标控制器的耦合,引入封装好的类文件后,只需少量代码,就可以实现一个基础的转场动画。

首先,我们来看一下实现一个普通的转场动画的流程。

设置转场方式

将转场方式设置为自定义

1
toVC.modalPresentationStyle = .custom

设置转场代理

这里转场代理可以设置为目标控制器,也可以是我们自定义的管理转场的类,这里设置为目标控制器

1
toVC.transitioningDelegate = toVC

实现转场代理方法

转场的代理方法我们需要实现以下两个方法,指定我们自定义的类来管理转场动画,关于这个自定义的类,后面会详细说。

1
2
3
4
5
6
7
8
9
10
11
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//这里的animationTransitionContr就是自定义的用来管理转场的类
animationTransitionContr.isPresenting = true
return animationTransitionContr
}

public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//这里的animationTransitionContr就是自定义的用来管理转场的类
animationTransitionContr.isPresenting = false
return animationTransitionContr
}

自定义转场动画管理类

这个管理类定为NSObject子类就可以,关键是它必须遵从UIViewControllerAnimatedTransitioning协议,并实现以下几个代理方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.25
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: .from) else { return }
guard let toVC = transitionContext.viewController(forKey: .to) else { return }
if isPresenting {
// 进入目标控制器的动画
} else {
// 退出目标控制器的动画
}

}

至此,一个常规的自定义转场动画就完成了。到这里就会发现一个问题,在实现具体动画的这一步,只有单个控制器使用这个转场类还好,如果是多个控制器分别需要实现不同的动画效果,这里的逻辑就会很复杂,代码可读性也不好。

要解决这个问题,我们可以把重构一下动画逻辑,将动画代码放在转场类里面,而将动画的具体实现放在目标控制器里。具体实现如下:

为转场控制类定义代理

这里的代理方法是将动画的不同时间点的控制暴露给目标控制器

1
2
3
4
5
6
7
8
	protocol ZYAnimationTransitionControllerDelegate: NSObjectProtocol {
func willPresent(fromView: UIView, toView: UIView)
func onPresent(fromView: UIView, toView: UIView)
func didPresent(fromView: UIView, toView: UIView)
func willDismiss(fromView: UIView, toView: UIView)
func onDismiss(fromView: UIView, toView: UIView)
func didDismiss(fromView: UIView, toView: UIView)
}

在适当的时机调用代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fileprivate func present(transitionContext: UIViewControllerContextTransitioning, container: UIView, fromView: UIView, toView: UIView, completion: @escaping () -> Void) {
container.addSubview(toView)
guard let delegate = delegate else { return }
delegate.willPresent(fromView: fromView, toView: toView)
self.startAnimation(transitionContext: transitionContext, animations: {
delegate.onPresent(fromView: fromView, toView: toView)
}) {
delegate.didPresent(fromView: fromView, toView: toView)
completion()
}
}

fileprivate func dismiss(transitionContext: UIViewControllerContextTransitioning, container: UIView, fromView: UIView, toView: UIView, completion: @escaping () -> Void) {
container.addSubview(fromView)
guard let delegate = delegate else { return }
delegate.willDismiss(fromView: fromView, toView: toView)
self .startAnimation(transitionContext: transitionContext, animations: {
delegate.onDismiss(fromView: fromView, toView: toView)
}) {
delegate.didDismiss(fromView: fromView, toView: toView)
completion()
}
}

这里使用了一个辅助方法:

1
2
3
4
5
6
7
8

fileprivate func startAnimation(transitionContext: UIViewControllerContextTransitioning, animations: @escaping () -> Void, completion: @escaping () -> Void) {
UIApplication.shared.beginIgnoringInteractionEvents()
UIView.animate(withDuration: self.transitionDuration(using: self as? UIViewControllerContextTransitioning), delay: 0, options: UIView.AnimationOptions(rawValue: 7 << 16), animations: animations, completion: { _ in
UIApplication.shared.endIgnoringInteractionEvents()
completion()
})
}

重构转场代理方法

用以下方法替换之前的代理方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: .from) else { return }
guard let toVC = transitionContext.viewController(forKey: .to) else { return }
if isPresenting {
present(transitionContext: transitionContext, container: containerView, fromView: fromVC.view, toView: toVC.view) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
} else {
dismiss(transitionContext: transitionContext, container: containerView, fromView: fromVC.view, toView: toVC.view) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}

至此,动画逻辑的重构就完成了。这时,一个完整的自定义转场的实现逻辑就是这样的:

  • 目标控制器设置转场类型为自定义
  • 目标控制器设置转场代理为自己
  • 目标控制器在转场代理方法中返回自定义的转场控制类
  • 在自定义转场控制类中实现UIViewControllerAnimatedTransitioning协议方法
  • 在自定义类中定义代理暴露动画控制权给目标控制器,并在适当时机调用代理方法

相关代码的完整实现可以参考我的图片预览框架源码