在日常开发中经常会用到push、pop、present、dismiss的操作,虽然系统有默认的动画,但是有时候会有不同的动画要求,比如,从底部动画弹出一个页面,同时这个页面的背景是有一定透明度,可以看到后面的页面。
这种情况下如果用系统自带的present动画,是无法实现的,当然,可以通过自定义的view,然后通过动画弹出,这种方式虽然也能实现的,但是会有一个很大的缺点,就是无法处理点击事件,得通过代理或者block的形式回调到所在的viewController,这样模块之间耦合太严重, 不够优雅。
基于上面的场景,我们可以通过自定义转场动画来优雅地实现这个需求。本文以自定义present、dismiss动画为例,详细过程如下。
实现的目标
可以自定义弹出方向,即可以从上、下、左、右和中间方向弹出,同时可以自定义背景颜色,和显示的视图。
整个转场过程涉及的协议主要有三个,分别如下
UIViewControllerTransitioningDelegate
视图控制器过渡协议,顾名思义,就是UIViewController需要签订的协议,用来告诉UIViewController,是由谁来负责转场动画的实现,协议定义和内容如下
public protocol UIViewControllerTransitioningDelegate : NSObjectProtocol {
@available(iOS 2.0, *)
optional func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
@available(iOS 2.0, *)
optional func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
optional func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
optional func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
@available(iOS 8.0, *)
optional func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?
从协议里面,我们发现了有两个方法含有present和dismiss
@available(iOS 2.0, *)
optional func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
@available(iOS 2.0, *)
optional func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
没错,这两个方法就是转场动画的主角,然后发现,这两个方法,返回的是一个签订了UIViewControllerAnimatedTransitioning协议的对象,接下来,就来看看这个协议的定义。
UIViewControllerAnimatedTransitioning
视图控制器动画过渡协议,顾名思义,就是用来告诉UIViewController,具体要以怎样的动画形式来完成转场,而我们自定义的转场动画,就是在这个协议的方法里面实现,协议的定义和内容如下
public protocol UIViewControllerAnimatedTransitioning : NSObjectProtocol {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
@available(iOS 10.0, *)
optional func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating
optional func animationEnded(_ transitionCompleted: Bool)
在这四个协议方法中,前两个是必需实现,也是最关键的
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
其中第一个方法,用来定义动画的时间,第二个方法就是用来实现具体的动画过程。而在这两个方法中,参数transitionContext是一个签订了UIViewControllerContextTransitioning协议的对象,接下来就看看这个协议又是做什么的。
UIViewControllerContextTransitioning
视图控制器上下文转换协议,顾名思义,就是获取到试图控制器在转场动画过程中的上下文,即从哪个UIViewController到哪个UIViewController,协议定义和内容如下
public protocol UIViewControllerContextTransitioning : NSObjectProtocol {
@available(iOS 2.0, *)
var containerView: UIView { get }
var isAnimated: Bool { get }
var isInteractive: Bool { get }
var transitionWasCancelled: Bool { get }
var presentationStyle: UIModalPresentationStyle { get }
func updateInteractiveTransition(_ percentComplete: CGFloat)
func finishInteractiveTransition()
func cancelInteractiveTransition()
@available(iOS 10.0, *)
func pauseInteractiveTransition()
func completeTransition(_ didComplete: Bool)
@available(iOS 2.0, *)
func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController?
@available(iOS 8.0, *)
func view(forKey key: UITransitionContextViewKey) -> UIView?
@available(iOS 8.0, *)
var targetTransform: CGAffineTransform { get }
@available(iOS 2.0, *)
func initialFrame(for vc: UIViewController) -> CGRect
@available(iOS 2.0, *)
func finalFrame(for vc: UIViewController) -> CGRect
协议内的方法很多,但是需要用到的并不多,最关键的几个方法
@available(iOS 2.0, *)
var containerView: UIView { get }
@available(iOS 2.0, *)
func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController?
func completeTransition(_ didComplete: Bool)
通过上面几个方法,我们可以获取到源控制器和目标控制器对应的view,返回自己定义动画,就能实现自定义转场动画。
通过上面的分析,我们了解到实现自定义转场动画,需要的过程大致如下
UIViewController通过签订UIViewControllerTransitioningDelegate协议,然后实现下面两个协议方法
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
分别返回两个签订了UIViewControllerAnimatedTransitioning协议的对象,用于实现present和dismiss动画过程。
而这个签订了UIViewControllerAnimatedTransitioning协议的对象,必须实现下面两个协议方法
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
定义整个转场动画的时间和具体的实现,而在transitionContext参数中,我们可以通过获取源视图控制器和目标视图控制器的UIView,从而实现自定义动画。
上面这几个必须实现的协议方法,可以直接在UIViewController里面签订和实现,但是这样会使UIViewController代码太复杂,不够优雅,所以我们可以通过将部分实现抽取出来,交给一个自定义对象来处理。然后再将整个转场动画抽取成一个基类,只要继承这个基类就能实现动画,同时又能各自管理自己的视图显示。具体代码实现过程如下。
AlertAnimationStyle
定义的动画类型枚举
enum AlertAnimationStyle {
case present
case dismiss
AlertAnimationPosition
定义的动画弹出位置枚举
动画弹出位置
enum AlertAnimationPosition {
case bottom
case top
case left
case right
case center
AlertTransitionAnimator
转场动画管理器,签订了UIViewControllerAnimatedTransitioning协议,实现具体的动画效果,代码如下
import UIKit
转场动画管理器
class AlertTransitionAnimator: NSObject {
var duration = 0.25
var animationStyle:AlertAnimationStyle = .present
var animationPosition:AlertAnimationPosition = .bottom
var maskColor:UIColor = UIColor.black.withAlphaComponent(0.5)
private var maskView:UIView?
convenience init(animationStyle:AlertAnimationStyle, animationPosition:AlertAnimationPosition) {
self.init()
self.animationStyle = animationStyle
self.animationPosition = animationPosition
extension AlertTransitionAnimator {
private func presentTransition(transitionContext:UIViewControllerContextTransitioning) {
guard let fromController = transitionContext.viewController(forKey: .from), let toController = transitionContext.viewController(forKey: .to), let fromView = fromController.view, let toView = toController.view else { return }
let contentView = transitionContext.containerView
fromView.tintAdjustmentMode = .normal
fromView.isUserInteractionEnabled = false
toView.isUserInteractionEnabled = false
contentView.addSubview(toView)
let maskView = UIView(frame: fromView.bounds)
maskView.backgroundColor = maskColor
maskView.alpha = 0
fromView.addSubview(maskView)
self.maskView = maskView
switch animationPosition {
case .top:
toView.frame = CGRect(x: 0, y: -contentView.bounds.size.height, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .bottom:
toView.frame = CGRect(x: 0, y: contentView.bounds.size.height, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .left:
toView.frame = CGRect(x: -contentView.bounds.size.width, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .right:
toView.frame = CGRect(x: contentView.bounds.size.width, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .center:
toView.frame = CGRect(x: 0, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
toView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
toView.alpha = 0
UIView.animate(withDuration: duration, animations: {
maskView.alpha = 1
switch self.animationPosition {
case .top, .bottom, .left, .right:
toView.frame = CGRect(x: 0, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .center:
toView.transform = CGAffineTransform.identity
toView.alpha = 1
}) { (finish) in
fromView.isUserInteractionEnabled = true
toView.isUserInteractionEnabled = true
transitionContext.completeTransition(true)
private func dismissTransition(transitionContext:UIViewControllerContextTransitioning) {
guard let fromController = transitionContext.viewController(forKey: .from), let toController = transitionContext.viewController(forKey: .to), let fromView = fromController.view, let toView = toController.view else { return }
let contentView = transitionContext.containerView
toView.tintAdjustmentMode = .normal
fromView.isUserInteractionEnabled = false
toView.isUserInteractionEnabled = false
UIView.animate(withDuration: duration, animations: {
self.maskView?.alpha = 0
/// 根据动画弹出位置,计算源视图动画完成后的frame
switch self.animationPosition {
case .top:
fromView.frame = CGRect(x: 0, y: -contentView.bounds.size.height, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .bottom:
fromView.frame = CGRect(x: 0, y: contentView.bounds.size.height, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .left:
fromView.frame = CGRect(x: -contentView.bounds.size.width, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .right:
fromView.frame = CGRect(x: contentView.bounds.size.width, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .center:
fromView.alpha = 0
}) { (finish) in
self.maskView?.removeFromSuperview()
fromView.removeFromSuperview()
toView.isUserInteractionEnabled = true
transitionContext.completeTransition(true)
extension AlertTransitionAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
switch animationStyle {
case .present:
presentTransition(transitionContext: transitionContext)
case .dismiss:
dismissTransition(transitionContext: transitionContext)
BaseAlertController
转场动画的基类,继承的子类可以自定义弹出方向,代码如下
import UIKit
基础弹窗控制器,可以实现从上下左右和中间位置动画弹出
使用方法:
子类只需要自定义初始化方法,然后设置弹出位置
convenience init() {
self.init(animationPosition: .right)
然后 let vc = ChildrenController() present(vc, animated: true, completion: nil)
class BaseAlertController: UIViewController {
deinit {
print("--__--|| \(type(of: self)) dealloc")
var duration = 0.25
var maskColor:UIColor = UIColor.black.withAlphaComponent(0.5)
var autoFall:Bool = true
private var animationPosition:AlertAnimationPosition = .bottom
private var transitionAnimator:AlertTransitionAnimator?
private lazy var panel:UIControl = {
$0.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
$0.backgroundColor = .clear
$0.addTarget(self, action: #selector(actionForPanel), for: .touchUpInside)
return $0
}(UIControl())
init(animationPosition:AlertAnimationPosition) {
super.init(nibName: nil, bundle: nil)
alertInitialize()
self.animationPosition = animationPosition
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
alertInitialize()
required init?(coder: NSCoder) {
super.init(coder: coder)
alertInitialize()
fatalError("init(coder:) has not been implemented")
override func loadView() {
view = panel
extension BaseAlertController {
private func alertInitialize() {
modalPresentationStyle = .custom
transitioningDelegate = self
@objc private func actionForPanel() {
if autoFall {
dismiss(animated: true)
extension BaseAlertController : UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator = AlertTransitionAnimator(animationStyle: .present, animationPosition: animationPosition)
transitionAnimator?.maskColor = maskColor
transitionAnimator?.duration = duration
return transitionAnimator
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator?.animationStyle = .dismiss
return transitionAnimator
class TopAlertController: BaseAlertController {
convenience init() {
self.init(animationPosition: .top)
override func viewDidLoad() {
super.viewDidLoad()
let height:CGFloat = 400
let label = UILabel(frame: CGRect(x: 0, y: 0, width: view.bounds.size.width, height: height))
label.backgroundColor = .white
label.textColor = .red
label.textAlignment = .center
label.numberOfLines = 0
label.text = "我是从上往下弹出的AlertController"
view.addSubview(label)
class RightAlertController: BaseAlertController {
convenience init() {
self.init(animationPosition: .right)
maskColor = UIColor.red.withAlphaComponent(0.2)
override func viewDidLoad() {
super.viewDidLoad()
let width:CGFloat = 250
let label = UILabel(frame: CGRect(x: view.bounds.size.width - width, y: 0, width: width, height: view.bounds.size.height))
label.backgroundColor = .white
label.textColor = .red
label.textAlignment = .center
label.numberOfLines = 0
label.text = "我是从右往左弹出的AlertController"
view.addSubview(label)
class CenterAlertController: BaseAlertController {
convenience init() {
self.init(animationPosition: .center)
maskColor = .blue.withAlphaComponent(0.2)
override func viewDidLoad() {
super.viewDidLoad()
print(view.frame)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
label.center = view.center
label.backgroundColor = .white
label.textColor = .red
label.textAlignment = .center
label.numberOfLines = 0
label.text = "我是从中间弹出的AlertController"
view.addSubview(label)