相关文章推荐
咆哮的抽屉  ·  electron ...·  1 周前    · 
打篮球的啄木鸟  ·  【进制转换】— ...·  3 周前    · 
没读研的鸡蛋面  ·  Python+tkinter+Treevie ...·  1 年前    · 
安静的小刀  ·  Io 异常: The Network ...·  1 年前    · 

在日常开发中经常会用到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 } // This indicates whether the transition is currently interactive.
    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
// MARK: private
extension AlertTransitionAnimator {
    /// present转场
    private func presentTransition(transitionContext:UIViewControllerContextTransitioning) {
        /// 获取源控制器的view和目标控制器的view
        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
        /// 视图的tintColor属性返回完全未修改的视图着色颜色
        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
        /// 根据动画弹出位置,计算目标视图开始动画前的frame
        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
        /// 动画转场,确定目标视图最后的frame
        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)
    /// dismiss转场
    private func dismissTransition(transitionContext:UIViewControllerContextTransitioning) {
        /// 获取源控制器的view和目标控制器的view
        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
        /// 视图的tintColor属性返回完全未修改的视图着色颜色
        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)
// MARK: UIViewControllerAnimatedTransitioning
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)
    /// 点击背景自动dismiss
    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())
    /// 子类可以自定义方法,可以定义弹出的位置
    /// convenience init() {
    ///    self.init(animationPosition: .bottom)
    /// }
    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
// MARK: private
extension BaseAlertController {
    private func alertInitialize() {
        modalPresentationStyle = .custom
        transitioningDelegate = self
    @objc private func actionForPanel() {
        if autoFall {
            dismiss(animated: true)
// MARK: UIViewControllerTransitioningDelegate
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)

分类:
iOS
标签: