前言
本文是对于自定义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的交互是一件很有意思的事,实现起来也不算难,可以说比较简单,只要了解大部分方法以及作用即可,但有很多细节还需要实践才能理解。
来源:CSDN
作者:LANNAWU
链接:https://blog.csdn.net/weixin_41473928/article/details/104282079