自定义ViewController的切换

帅比萌擦擦* 提交于 2020-02-15 12:40:36

前言

本文是对于自定义ViewController专场动画的实战,内容有普通无交互式的切换到随着手势变化的切换,是我对这一内容的总结与实战,文章属于原创,转载请注明出处。

本文实现了一个从上方掉落并带有弹性效果的非交互式动画,和一个从上方用手指下拉,若没到指定位置(屏幕的百分之50)处则不转换的交互式动画,因为我不喜欢使用VC的容器,所以本篇文章不涉及VC容器间的转换,但容器VC间的转换方式也大同小异,几乎没有区别。

参考文章:Objc.IO,王巍的博客

无交互式切换

Protocol UIViewControllerContextTransitioning

  • 这个接口提供了MVC切换时的上下文,和各种个样的信息,但要记住,文档里有一句话写到,不要自己写一个Conform这个Protocol的类或者对象。
  • ContainerView: 这个属性是记录了当前MVC切换时堆栈的情况,是多个MVC的容器,在我看来所谓ViewController是一个抽象的类,里面是一个以View做为ViewController的RootView,接下来所有自定义的View都做为这个RootView的SubViews存在于图形堆栈中,ContainerView则维护着所有的VC的RootView的情况,并在做动画时,加入或者删除VC的View
  • viewControllerForKey: 这个方法返回一个结构体,里面有两种情况分别是,.To和.From,分别代表着专场到的VC和将要被专场的VC,在这里理解这个关系很重要,看似简单,但其实还是经得起一点考虑
  • initialFrameForViewController: 一个VC初始的位置,可以用来计算位置
  • finalFrameForViewController: 一个VC最后的位置,切换结束时MVC的位置
  • completeTransition: didComplete: 向这个Context报告已经转换完成

Protocol UIViewControllerAnimatedTransitioning

这个接口负责实现具体的内容,动画做什么,和具体的实现,使用时应该自定义自己的类,然后Conform这个Protocol

  • transitionDuration(transitionContext: UIViewControllerTransitionContext): 这个方法返回一个TimeInterval,使用这个方法来决定当前动画的持续时间
  • animateTransition(transitionContext: UIViewControllerTransitionContext): 使用这个方法,可以自定义动画的实现,一半情况下,对VC专场的动画,实际上是在对RootView做动画,使用UIView的静态方法都在当前方法中完成

Protocol UIViewControllerTransitioningDelegate

对需要实现Present和Dismiss的VC要Conform这个Delegate,然后实现对应的方法即可

  • animationControllerForPresentController:
  • animationControllerForDismissController:
  • interactionControllerForPresentation:
  • interactionControllerForDismiss:
    前两个方法是无交互式切换VC的

Demo

写再多不如一个Demo

import UIKit

protocol LNSecondViewControllerDelegate: NSObjectProtocol {
    func secondViewControllerDidClickDismissButton(viewController: LNSecondViewController)
}

class LNSecondViewController: UIViewController
{
    var delegate: LNSecondViewControllerDelegate?
    
    private var button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let size = CGSize(width: 150.0, height: 150.0)
        let origin = CGPoint(x: (view.frame.width - 150.0) / 2.0, y: (view.frame.height - 150.0) / 2.0)
        button.frame = CGRect(origin: origin, size: size)
        button.backgroundColor = .link
        button.layer.masksToBounds = true
        button.layer.cornerRadius = 75.0
        
        let selector = #selector(handleDismissGesture)
        button.addTarget(self, action: selector, for: .touchUpInside)
                
        view.addSubview(button)
    }
    
    @objc private func handleDismissGesture(_ sender: UIButton) {
        if self.delegate != nil {
            self.delegate?.secondViewControllerDidClickDismissButton(viewController: self)
        }
    }
}

先定义一个类和其中对应的Protocol和Delegate,并添加一个button然后为button添加事件

import UIKit

class LNFirstViewController: UIViewController, LNSecondViewControllerDelegate, UIViewControllerTransitioningDelegate
{
    private var button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let size = CGSize(width: 150.0, height: 150.0)
        let origin = CGPoint(x: (view.frame.width - 150.0) / 2.0, y: (view.frame.height - 150.0) / 2.0)
        button.frame = CGRect(origin: origin, size: size)
        
        button.backgroundColor = .link
        button.layer.masksToBounds = true
        button.layer.cornerRadius = 12.0
        
        let selector = #selector(clickToDissmiss)
        button.setTitle("Present", for: .normal)
        button.addTarget(self, action: selector, for: .touchUpInside)
        
        view.backgroundColor = .orange
        
        view.addSubview(button)
        print("ViewDidLoad")
    }
    
    func secondViewControllerDidClickDismissButton(viewController: LNSecondViewController) {
        viewController.dismiss(animated: true)
    }
    
    @objc private func clickToDissmiss(_ sender: UIButton) {
        let mvc = LNSecondViewController()
        
        mvc.delegate = self
        
        transitionController.wireToViewController(viewController: mvc)
        
        present(mvc, animated: true)
    }

先使用标准的MVC返回创建样例,测试之后发现没有问题

然后接下来定义一个类,并继承UIViewControllerAnimatedTransitioning

class LNBouncesAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning
{
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1.0
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let toVC = transitionContext.viewController(forKey: .to) else { return }
        
        let screenBounds = UIScreen.main.bounds
        let initFrame = screenBounds.offsetBy(deltaX: 0.0, deltaY: -screenBounds.height)
        let finalFrame = CGRect(x: 0.0, y: 0.0, width: screenBounds.width, height: screenBounds.height)
        toVC.view.frame = initFrame
        transitionContext.containerView.addSubview(toVC.view)
        
        let duration = self.transitionDuration(using: transitionContext)
        UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.40, initialSpringVelocity: 0.0, options: .curveLinear, animations: {
            toVC.view.frame = finalFrame
        }) { (completion) in
            transitionContext.completeTransition(true)
        }
    }
    
    
}
  • 此时,使用transitionDuration: 当前方法返回一个TimeInterval,代表动画执行的时间
  • 然后初始化它的View是在屏幕下方,然后弹出到指定位置。
  • 将View添加到ContainerView当中
  • 开始动画,使用UIView的动画方法来实现这个动画,前文我也有提过,VC之间转换的动画,实际上就是RootView间的动画
  • 动画结束后,要把是否成功的状态发送给上下文
class LNFirstViewController: UIViewController, LNSecondViewControllerDelegate, UIViewControllerTransitioningDelegate
{
    private var presentingAnimation = LNBouncesAnimatedTransitioning()

    func secondViewControllerDidClickDismissButton(viewController: LNSecondViewController) {
        viewController.dismiss(animated: true)
    }
    
    @objc private func clickToDissmiss(_ sender: UIButton) {
        let mvc = LNSecondViewController()
        
        mvc.delegate = self
        
        mvc.transitioningDelegate = self
                
        present(mvc, animated: true)
    }
    
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self.presentingAnimation
    }
  • 一定要Conform UIViewControllerTransitioningDelegate
  • 然后简单的实现Delegate的方法即可实现动画

可交互的VC转换

可交互的VC转换是当你滑动屏幕,当你改变主意,不想返回时,只要你放开手或者将VC拖动到百分之五十部分,即可回到当前屏幕,取消打开下一个页面,现在很多APP也已经在使用这个功能,这个功能也很常见,现在来实现一下

UIPercentDrivenInteraciveTransition

先来介绍一下这个类的方法

  • updataInteractiveTransition(percentComplete:): 更新的百分比,一般通过手势进行的长度来进行计算一个值,然后进行更新
  • cancelInteractiveTransition: 取消交互,报告当前状态
  • finishInteractiveTransition: 交互完成,报告当前状态

class LNSwipeUpPercentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition
{   
    var isInteracting = false
    
    private var shouldComplete = false
    
    private var presentingVC = UIViewController()
    
    func wireToViewController(viewController: UIViewController) {
        presentingVC = viewController
        let gestureSEL = #selector(handlePanGesture)
        let pan = UIPanGestureRecognizer(target: self, action: gestureSEL)
        presentingVC.view.addGestureRecognizer(pan)
    }
    
    @objc private func handlePanGesture(_ recognier: UIPanGestureRecognizer) {
        let translation = recognier.translation(in: recognier.view?.superview)
        switch recognier.state {
        case .began:
            self.isInteracting = true
            self.presentingVC.dismiss(animated: true)
            break
        case .changed:
            var fraction = translation.y / 834.0
            fraction = min(max(fraction, 0.0), 1.0) // 最小 0.0,最大 1.0
            self.shouldComplete = (fraction > 0.5)
            
            self.update(fraction)
        case .ended:
            fallthrough
        case .cancelled:
            self.isInteracting = false
            if !self.shouldComplete || recognier.state == .cancelled {
                cancel()
            } else {
                finish()
            }
        default:
            break
        }
    }
}
  • 这段代码只做了一件事,把一个Pan手势的事件绑定到要进行VC交互的RootView上
  • 变量shouldComplete在当前的类里判断手势滑动是否有超过百分之50,当过了百分之50就设为true,不然设为false
  • 然后使用变量interacting来记录手势是否完成,返回给VC来判断是否完成手势

完成最后的动画

完成动画需要自己实现一个类符合Protocol UIViewControllerAnimatedTransitioning 就和前面做的一样,代码上没有太大区别,自己实现一遍还可以让你理解toVC和fromVC之间的关系,本文在这里就不再写一遍了。

最后在FirstViewController处简单的加上几段代码就完成了

    private var dissmissingAnimation = LNNormalAnimatedTransition()
    
    private var transitionController = LNSwipeUpPercentDrivenInteractiveTransition()


    @objc private func clickToDissmiss(_ sender: UIButton) {
        
        transitionController.wireToViewController(viewController: mvc)
        
    }

    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return transitionController.isInteracting ? self.transitionController : nil
    }
  • 定义一个 UIPercentDrivenInteractiveTransition 的实例,并定义一个自己实现的无交互的VC切换的实例,并实现Protocol的方法即可

总结: VC的交互是一件很有意思的事,实现起来也不算难,可以说比较简单,只要了解大部分方法以及作用即可,但有很多细节还需要实践才能理解。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!