iOS UIViewController自定义转场动画

1. 前言

上一篇文章讲述了基于CALayer的系统转场动画,本篇文章将会探究一下UIViewController自定义转场动画,在文中还会展示一些自定义的动画效果。

本篇文章主要探究一下基于UINavigationControllerDelegate协议的push和pop动画,以及基于UIViewControllerTransitioningDelegate协议的present和dismiss动画,另外还要说明一点文中的涉及到的以及demo中的代码都是点击交互动画,不包括滑动交互动画。

测试环境:Xcode12.3,Swift5.0以上,iOS13及以上系统设备。

2. UIViewControllerAnimatedTransitioning协议

在了解上面两个协议之前,先来看看这个UIViewControllerAnimatedTransitioning协议,该协议提供了一组实现自定义视图控制器转换动画的方法。比如常用的两个:

// 该方法中返回动画要执行的时间,单位秒,必须实现的协议方法。
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval

// 动画执行的方法。必须实现的协议方法。
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)

通过这些协议方法,我们可以定义一个animator对象,它为视图的转换提供动画支持。

比如下面这个Transition类:

public class Transition: NSObject {
    public var operation: TransitionOperation = .push
    public var context: UIViewControllerContextTransitioning?
    
    func executePushOrPresentTransition() { }
    func executePopOrDismissTransition() { }
}

extension Transition: UIViewControllerAnimatedTransitioning {
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        context = transitionContext
        if operation == .push || operation == .present {
            executePushOrPresentTransition()
        }else if operation == .pop || operation == .dismiss {
            executePopOrDismissTransition()
        }
    }
}

extension Transition: CAAnimationDelegate {
    public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        context?.completeTransition(true)
        if operation == .push || operation == .present {
            let toVC = context?.viewController(forKey: .to)
            toVC?.view.mask = nil
        }else if operation == .pop || operation == .dismiss {
            let fromVC = context?.viewController(forKey: .from)
            fromVC?.view.mask = nil
        }
    }
}

如果我们实例化一个Transition对象,则就是上面说到的animator对象。

Transition类中还可以实现CAAnimationDelegate协议方法,这样可以监听CAAnimation动画的状态,进而做一些事情。

3. UINavigationControllerDelegate协议

该协议中,如果要实现自定义的点击转场动画,则需要实现下面的协议方法:

// 在该方法中需要返回一个实现了UIViewControllerAnimatedTransitioning协议的类的实例对象。
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?

Operation则表示是push还是pop,定义如下:

    public enum Operation : Int {

        case none = 0

        case push = 1

        case pop = 2
    }

至于fromVC和toVC,不用说大家也明白。

该协议方法要求返回一个实现了UIViewControllerAnimatedTransitioning协议的类的实例对象,在上面已经说到了这个协议。

对了要想实现UINavigationControllerDelegate协议的方法,别忘了设置delegate哦!该代理最好每次都设置,最好在viewWillAppearviewWillDisappear中设置。

weak open var delegate: UINavigationControllerDelegate?

示例:

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationController?.delegate = self
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.navigationController?.delegate = nil
    }
extension ViewController: UINavigationControllerDelegate {
    // 该方法中返回一个上面说的animator对象,也就是Transition类的实例对象。
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        if operation == .push {
            return createTransition(operation: .push)
        }else if operation == .pop {
            return createTransition(operation: .pop)
        }else {
            return nil
        }
    }
}

4. UIViewControllerTransitioningDelegate协议

该协议中,如果要实现自定义的点击转场动画,则需要实现下面的协议方法:

// 当present的时候调用该方法
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?

// 当dismiss的时候调用该方法
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?

这两个协议方法要求返回一个实现了UIViewControllerAnimatedTransitioning协议的类的实例对象,在上面已经说到了这个协议。

对了要想实现UIViewControllerTransitioningDelegate协议的方法,同样需要设置代理,这个代理名字可和上面不一样了,该代理最好每次都在调用present和dismiss方法的地方设置。

weak open var transitioningDelegate: UIViewControllerTransitioningDelegate?

另外我们需要设置被弹出的ViewController的modalPresentationStyle属性为fullScreen或者currentContext的时候,动画正常,否则不会有自定义动画效果或者动画异常。至少在笔者的测试案例里面是这样的。

示例代码:

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let type = typeArray[indexPath.row]
        selectedTransitionType = type
        if type == .circle {
            clickView = tableView.cellForRow(at: indexPath)?.contentView
        }
        let detailVC = DetailViewController(nibName: "DetailViewController", bundle: nil)
        if indexPath.section == 0 {
            self.navigationController?.pushViewController(detailVC, animated: true)
        }else {
            detailVC.modalPresentationStyle = .fullScreen
            detailVC.transitioningDelegate = self
            self.present(detailVC, animated: true, completion: nil)
        }
    }

extension ViewController: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return createTransition(operation: .present)
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return createTransition(operation: .dismiss)
    }
}

5. 测试Demo

先看一组自定义动画:

Push和Pop:

          

Present和Dismiss:

          

下面就以圆形动画转场来说明一下:

上面不管是Push/Pop还是Present/Dismiss的协议方法,里面都调用了下面的方法来创建一个自定义动画:

    // 创建自定义动画
    func createTransition(operation: TransitionOperation) -> Transition? {
        switch selectedTransitionType {
        case .circle:
            let animation = CircleTransition()
            animation.operation = operation
            animation.clickView = clickView
            return animation
        case .boom:
            let animation = BoomTransition()
            animation.operation = operation
            return animation
        case .spreadFromRight:
            let animation = SpreadTransition()
            animation.direction = .fromRight
            animation.operation = operation
            return animation
        case .spreadFromLeft:
            let animation = SpreadTransition()
            animation.direction = .fromLeft
            animation.operation = operation
            return animation
        case .spreadFromTop:
            let animation = SpreadTransition()
            animation.direction = .fromTop
            animation.operation = operation
            return animation
        case .spreadFromBottom:
            let animation = SpreadTransition()
            animation.direction = .fromBottom
            animation.operation = operation
            return animation
        default:
            return nil
        }
    }

根据点击不同的cell,从而选择不同的动画。

整体架子就是这样,主要的就是在动画实现部分,Demo中定义了一个Super类Transition,见下面代码,其他的自定义的动画类都继承这个类。

import UIKit

// 自定义的行为方式。
public enum TransitionOperation: Int {
    case push
    case pop
    case present
    case dismiss
}

// 动画类型枚举
public enum TransitionType: Int {
    case none = 0           // 系统默认
    case circle             // 圆形效果
    case boom               // 爆炸效果
    case spreadFromRight    // 从右侧展开进入
    case spreadFromLeft     // 从左侧展开进入
    case spreadFromTop      // 从上面展开进入
    case spreadFromBottom   // 从底部展开进入
    
    var index: Int {
        return self.rawValue
    }
    
    var text: String {
        switch self {
        case .circle:
            return "Circle"
        case .boom:
            return "Boom"
        case .spreadFromRight:
            return "SpreadFromRight"
        case .spreadFromLeft:
            return "SpreadFromLeft"
        case .spreadFromTop:
            return "SpreadFromTop"
        case .spreadFromBottom:
            return "SpreadFromBottom"
        default:
            return "default"
        }
    }
}

public class Transition: NSObject {
    public var operation: TransitionOperation = .push
    public var context: UIViewControllerContextTransitioning?
    
    // 当是Push或者Present的时候执行该方法
    func executePushOrPresentTransition() { }
    
    // 当是Pop或者Dismiss的时候执行该方法
    func executePopOrDismissTransition() { }
}

extension Transition: UIViewControllerAnimatedTransitioning {
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        context = transitionContext
        if operation == .push || operation == .present {
            executePushOrPresentTransition()
        }else if operation == .pop || operation == .dismiss {
            executePopOrDismissTransition()
        }
    }
}

extension Transition: CAAnimationDelegate {
    public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        context?.completeTransition(true)
        if operation == .push || operation == .present {
            // 动画结束后,移除mask。
            let toVC = context?.viewController(forKey: .to)
            toVC?.view.mask = nil
        }else if operation == .pop || operation == .dismiss {
            // 动画结束后,移除mask。
            let fromVC = context?.viewController(forKey: .from)
            fromVC?.view.mask = nil
        }
    }
}

子类只需要复写父类的方法即可,比如圆形转场效果:

import UIKit

// 圆形放大缩小动画
public class CircleTransition: Transition {
    // 外界传入的点击视图
    public var clickView: UIView?
    
    // Push和Present执行的动画
    override func executePushOrPresentTransition() {
        // 通过UIViewControllerContextTransitioning获取视图容器
        guard let containerView = context?.containerView else { return }
        // 获取当前Controller和将要跳转的Controller
        guard let fromVC = context?.viewController(forKey: .from), let toVC = context?.viewController(forKey: .to) else { return }
        
        // 将讲个VC的view添加到容器中,默认容器中会有一个当前的VC视图。重复添加也没关系,只会改变层级关系。
        containerView.addSubview(fromVC.view)
        containerView.addSubview(toVC.view)
        
        // 计算点击视图的在容器视图中的位置。
        var clickPoint: CGPoint = .zero
        if let startView = clickView {
            clickPoint = startView.convert(startView.center, to: containerView)
        }

        // 画小圆路径,先设置小圆半径为1.
        let smallPath = UIBezierPath(arcCenter: clickPoint, radius: 1, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
        
        // 画大圆,大圆的半径取点击点到屏幕四个角距离的最大值。
        let topLeftDistance = sqrtf(Float(pow(clickPoint.x, 2) + pow(clickPoint.y, 2)))
        let topRightDistance = sqrtf(Float(pow(containerView.bounds.size.width - clickPoint.x, 2) + pow(clickPoint.y, 2)))
        let bottomLeftDistance = sqrtf(Float(pow(clickPoint.x, 2) + pow(containerView.bounds.size.height - clickPoint.y, 2)))
        let bottomRightDistance = sqrtf(Float(pow(containerView.bounds.size.width - clickPoint.x, 2) + pow(containerView.bounds.size.height - clickPoint.y, 2)))
        
        let bigCircleRadius = max(max(topLeftDistance, topRightDistance), max(bottomLeftDistance, bottomRightDistance))
        
        // 画大圆路径
        let bigPath = UIBezierPath(arcCenter: clickPoint, radius: CGFloat(bigCircleRadius), startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
        
        // 画圆
        let maskLayer = CAShapeLayer()
        maskLayer.path = bigPath.cgPath
        toVC.view.layer.mask = maskLayer
        // 创建动画,从小圆变化到大圆。
        let animation = CABasicAnimation(keyPath: "path")
        animation.duration = self.transitionDuration(using: context)
        animation.fromValue = smallPath.cgPath
        animation.toValue = bigPath.cgPath
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        animation.delegate = self
        
        maskLayer.add(animation, forKey: "maskLayerAnimation")
    }
    
    override func executePopOrDismissTransition() {
        guard let containerView = context?.containerView else { return }
        guard let fromVC = context?.viewController(forKey: .from), let toVC = context?.viewController(forKey: .to) else { return }
        
        containerView.addSubview(toVC.view)
        containerView.addSubview(fromVC.view)
        
        var clickPoint: CGPoint = .zero
        if let startView = clickView {
            clickPoint = startView.convert(startView.center, to: containerView)
        }
        
        let smallPath = UIBezierPath(arcCenter: clickPoint, radius: 1, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
        
        let topLeftDistance = sqrtf(Float(pow(clickPoint.x, 2) + pow(clickPoint.y, 2)))
        let topRightDistance = sqrtf(Float(pow(containerView.bounds.size.width - clickPoint.x, 2) + pow(clickPoint.y, 2)))
        let bottomLeftDistance = sqrtf(Float(pow(clickPoint.x, 2) + pow(containerView.bounds.size.height - clickPoint.y, 2)))
        let bottomRightDistance = sqrtf(Float(pow(containerView.bounds.size.width - clickPoint.x, 2) + pow(containerView.bounds.size.height - clickPoint.y, 2)))
        
        let bigCircleRadius = max(max(topLeftDistance, topRightDistance), max(bottomLeftDistance, bottomRightDistance))
        
        let bigPath = UIBezierPath(arcCenter: clickPoint, radius: CGFloat(bigCircleRadius), startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
        
        let maskLayer = CAShapeLayer()
        maskLayer.path = smallPath.cgPath
        fromVC.view.layer.mask = maskLayer
        
        // 创建动画,从大圆变化到小圆。
        let animation = CABasicAnimation(keyPath: "path")
        animation.duration = self.transitionDuration(using: context)
        animation.fromValue = bigPath.cgPath
        animation.toValue = smallPath.cgPath
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        animation.delegate = self
        
        maskLayer.add(animation, forKey: "maskLayerAnimation")
    }
    
}

6. 结束语

本文主要介绍了UIViewController自定义转场协议以及动画配置,在自定义动画部分没有详细讲解,配上Demo供大家参考:传送门

文中如有不正确或者可优化的地方,还请路过的朋友指正。

本篇文章出自https://blog.csdn.net/guoyongming925的博客,如需转载,请标明出处。

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的示例代码,展示了如何生成一个自定义的TabBar: 首先,创建一个名为CustomTabBarController自定义视图控制器,作为TabBar的容器: ```swift import UIKit class CustomTabBarController: UIViewController { // 自定义TabBar视图 let customTabBar = CustomTabBar() override func viewDidLoad() { super.viewDidLoad() // 添加自定义TabBar视图 view.addSubview(customTabBar) // 设置自定义TabBar的位置和大小 customTabBar.frame = CGRect(x: 0, y: view.frame.height - 100, width: view.frame.width, height: 100) // 设置自定义TabBar的按钮点击事件 customTabBar.button1.addTarget(self, action: #selector(button1Tapped), for: .touchUpInside) customTabBar.button2.addTarget(self, action: #selector(button2Tapped), for: .touchUpInside) customTabBar.button3.addTarget(self, action: #selector(button3Tapped), for: .touchUpInside) } // 按钮1点击事件 @objc func button1Tapped() { // 切换到第一个视图控制器 selectedIndex = 0 } // 按钮2点击事件 @objc func button2Tapped() { // 切换到第二个视图控制器 selectedIndex = 1 } // 按钮3点击事件 @objc func button3Tapped() { // 切换到第三个视图控制器 selectedIndex = 2 } } ``` 接下来,创建一个名为CustomTabBar的自定义TabBar视图,用于显示TabBar按钮: ```swift import UIKit class CustomTabBar: UIView { // TabBar按钮 let button1 = UIButton() let button2 = UIButton() let button3 = UIButton() override init(frame: CGRect) { super.init(frame: frame) // 设置按钮的样式、位置和大小 button1.setTitle("Tab 1", for: .normal) button1.frame = CGRect(x: 0, y: 0, width: frame.width / 3, height: frame.height) button2.setTitle("Tab 2", for: .normal) button2.frame = CGRect(x: frame.width / 3, y: 0, width: frame.width / 3, height: frame.height) button3.setTitle("Tab 3", for: .normal) button3.frame = CGRect(x: (frame.width / 3) * 2, y: 0, width: frame.width / 3, height: frame.height) // 添加按钮到自定义TabBar视图 addSubview(button1) addSubview(button2) addSubview(button3) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ``` 最后,在AppDelegate中设置CustomTabBarController为应用程序的主视图控制器: ```swift import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) // 创建自定义TabBar视图控制器 let customTabBarController = CustomTabBarController() // 设置自定义TabBar视图控制器为主视图控制器 window?.rootViewController = customTabBarController window?.makeKeyAndVisible() return true } } ``` 在这个示例中,我们创建了一个CustomTabBarController作为自定义TabBar的容器,并在其中添加了CustomTabBar视图。CustomTabBar视图中包含了三个按钮,分别用于切换到不同的视图控制器。你可以根据需要进行修改和扩展,以满足你的具体需求。 希望对你有所帮助!如果你有任何进一步的问题,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值