本篇介绍下转场动画的定制化,转场动画用的不多,但是还是会有需要在某些页面特地定制化的一些场景出现,今天抽了些时间,把框架理了一下,做成了可让Controller 自定义的场景.这篇,就是讲我封装的流程思路.
需求点
为什么转场动画会涉及到框架,因为跳转本身就需要框架.最普通的跳转如下:
let vc = NaviTransitionViewController()
self.navigationController?.pushViewController(vc, animated: true)
我们总不能啥都不封装然后用系统的吧,一但你这么做,你就将两个控制器耦合在了一起,这对项目以后的解耦合,组件化会产生很大的麻烦,甚至是有时候我们要砍一些业务,删代码都能痛苦死.我们绝大部分是通过路由跳转,路由的好处是,模块间完全不关心你的什么业务,你只需要告诉我,你的页面需要什么参数,就不用管了,所以,不管路由是如何的设计,我们大概率会是无控制器传参跳转.比如:
RoutePersonal.info(user: model).push()
转场的定制最好不要影响到路由,所以我们应该是需要封装到我们的路由里,这时候问题来了,该怎么封装,navigation 的 delegate 只能有一个,那么如何做到定制化呢
设计
方案一
在路由最后的跳转里,设置一个协议实现,将代理转发出去.
private static func doPush(_ viewController: UIViewController, nav: UINavigationController, animated: Bool) {
if let delegate = viewController as? NavigationControllerProtocol {
(nav as? RTRootNavigationController)?.delegate = delegate.navigationControllerDegate
} else {
nav.delegate = nil
}
guard let intent = viewController.intent as? Intent else {
nav.pushViewController(viewController, animated: animated)
return
}
// doSomePush
}
/// 遵循控制器
protocol NavigationControllerProtocol: NSObject {
var navigationControllerDegate: UINavigationControllerDelegate? { get }
var navigationOperationType: UINavigationController.Operation { get set }
}
然后让控制器去实现协议,进而实现转场动画代理逻辑.
extension NavAnimateController: NavigationControllerProtocol, UINavigationControllerDelegate, UIViewControllerAnimatedTransitioning {
var navigationControllerDegate: UINavigationControllerDelegate? {
self
}
/// 返回转场动画代理对象
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.navigationOperationType = operation
return self
}
}
我早上一开始是这么设计的,但是上了会儿厕所的功夫,这种方案被我推翻了,因为navigationController.viewControllers 共用一个控制器,我一但把delegate 权限提供出去,那么后就会有其他的问题,A->B->C这种界面跳转,B 持有 delegate,在跳转 C 时 delegate 传给 c,C在pop 回来时 B 就拿不到 delegate 了,因为我们不知道代理传给谁,这不是一种好的设计,.所以我又想到了第二种方案.
方案二
方案二是我最后决定使用的方案,采用之前 tableProvider 类似的设计,判断 controller 能否响应搭理&&controller 能否响应指定代理方法,从来将代理方法转发给 Controller
当然,具体实现也要看个人,我这里讲下我这里的设计,我目前制定的框架中,采用的是 RTNavigationController 做 navigationController,好处就是为了使用其每个页面都能持有一个自己的 navigationBar,并且可自定义 NavigationBar,但是,不好的地方就是三方的封装 NavigationController,我们不好去管理其参杂的业务逻辑,所以我这边是将代码引入,我这里是用一个 LocalPod 管理,然后其中的代码我都可以自己"魔改",不管你是用三方的,还是自己创建管理的 navigationController,设计的原理的策略都可以按照我这个来.
pod 'RTNavigationController', :path => 'LocalPod/RTNavigationController/'
我这里就不多讲,只讲涉及到我转场的逻辑
主要逻辑就是将 navigationControllerDelegate 中涉及的方法都转发给 Controller
/// 返回交互转场代理
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController
{
animationController = RTSafeUnwrapViewController(animationController);
// 先手动判断外部能否正常返回
if ([animationController conformsToProtocol:@protocol(UINavigationControllerDelegate)]) {
id<UINavigationControllerDelegate> delegate = (id<UINavigationControllerDelegate>)animationController;
if ([delegate respondsToSelector:@selector(navigationController:interactionControllerForAnimationController:)]) {
id responseDelegate = [delegate navigationController:navigationController interactionControllerForAnimationController:animationController];
if (responseDelegate != nil) {
return responseDelegate;
}
}
}
if ([animationController conformsToProtocol:@protocol(UIViewControllerInteractiveTransitioning)]) {
id<UIViewControllerInteractiveTransitioning> controller = (id<UIViewControllerInteractiveTransitioning>)animationController;
if (controller.wantsInteractiveStart) {
return controller;
} else {
return nil;
}
}
if ([self.rt_delegate respondsToSelector:@selector(navigationController:interactionControllerForAnimationController:)]) {
return [self.rt_delegate navigationController:navigationController
interactionControllerForAnimationController:animationController];
}
return nil;
}
/// push||pop 时调用的 delegate
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
fromVC = RTSafeUnwrapViewController(fromVC);
toVC = RTSafeUnwrapViewController(toVC);
// 判断 from 能否响应
if ([fromVC conformsToProtocol:@protocol(UINavigationControllerDelegate)]) {
id<UINavigationControllerDelegate> delegate = (id<UINavigationControllerDelegate>)fromVC;
if ([delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) {
id fromDelegate = [delegate navigationController:navigationController
animationControllerForOperation:operation
fromViewController:fromVC
toViewController:toVC];
if (fromDelegate != nil) {
return fromDelegate;
}
}
}
// 判断 to 能否响应
if ([toVC conformsToProtocol:@protocol(UINavigationControllerDelegate)]) {
id<UINavigationControllerDelegate> delegate = (id<UINavigationControllerDelegate>)toVC;
if ([delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) {
id toDelegate = [delegate navigationController:navigationController
animationControllerForOperation:operation
fromViewController:fromVC
toViewController:toVC];
if (toDelegate != nil) {
return toDelegate;
}
}
}
// 判断 rt_delegate 能否响应
if ([self.rt_delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) {
id rtDelegate = [self.rt_delegate navigationController:navigationController
animationControllerForOperation:operation
fromViewController:fromVC
toViewController:toVC];
if (rtDelegate != nil) {
return rtDelegate;
}
}
return nil;
}
这里判断了 from 能否响应,to能否响应,rt_delegate 能否响应,我们只需要前两个的逻辑即可.
然后在我们的自定义 Controller 中写下
/// 返回转场动画代理对象
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.navigationOperationType = operation
let realFrom = (fromVC as? RTContainerController)?.contentViewController ?? fromVC
let realTo = (toVC as? RTContainerController)?.contentViewController ?? toVC
if operation == .push && realTo is Self {// push&&toVC是我
return self
}
if operation == .pop && realFrom is Self {// pop&&fromVC是我
return self
}
return nil
}
然后就可以走正常的转场代理逻辑了
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.8.interval
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//获得容器视图(转场动画发生的地方)
let containerView = transitionContext.containerView
//动画执行时间
let duration = self.transitionDuration(using: transitionContext)
//fromVC (即将消失的视图)
let fromVC = transitionContext.viewController(forKey: .from)!
let fromView = fromVC.view!
//toVC (即将出现的视图)
let toVC = transitionContext.viewController(forKey: .to)!
let toView = toVC.view!
var offset = containerView.frame.height
var fromTransform = CGAffineTransform.identity
var toTransform = CGAffineTransform.identity
offset = navigationOperationType == .push ? -offset : offset
fromTransform = CGAffineTransform(translationX: 0, y: -offset)
toTransform = CGAffineTransform(translationX: 0, y: offset)
containerView.insertSubview(toView, at: 0)
toView.transform = toTransform
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.6, options: .curveEaseInOut) {
fromView.transform = fromTransform
toView.transform = .identity
} completion: { (finished) in
fromView.transform = .identity
toView.transform = .identity
//考虑到转场中途可能取消的情况,转场结束后,恢复视图状态。(通知是否完成转场)
let wasCancelled = transitionContext.transitionWasCancelled
transitionContext.completeTransition(!wasCancelled)
}
}
我在上述的代码里写了一个 demo,模拟了一个 navigation 的转场动画.
![](https://img.haomeiwen.com/i2217917/4ae6cd6f88cb9dcd.gif)
最后贴出一个完整的业务控制器代码,这里面我也顺便实现了交互的转场
import Foundation
import RTNavigationController
class NavAnimateController: UIViewController {
let btn = UIButton(.random)
let panGesture = UIPanGestureRecognizer()
var interactionTransition: UIPercentDrivenInteractiveTransition?
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .random
btn.add(to: self.view)
btn.snap {
$0.center.snap()
$0.width.height.equalTo(100)
}
self.rt_disableInteractivePop = true
btn.r.touchUpInside.observeValues {[weak self] _ in
guard let self = self else { return }
SecondController.push()
}
panGesture.addTarget(self, action: #selector(handlerPan(_:)))
self.view.addGestureRecognizer(panGesture)
log(self.navigationController?.view.frame ?? .zero)
}
deinit {
log("💀💀💀------------ \(Self.self)")
}
@objc func handlerPan(_ pan: UIScreenEdgePanGestureRecognizer) {
let translationY = panGesture.translation(in: view).y
let absY = abs(translationY)
let progress = absY / view.frame.height
log("\(progress)")
switch panGesture.state {
case .began:
// transitionUtil.interactive = true
//速度
self.interactionTransition = UIPercentDrivenInteractiveTransition()
self.navigationController?.popViewController(animated: true)
case .changed:
//更新转场进度,进度数值范围为0.0~1.0。
self.interactionTransition?.update(progress)
log("change")
case .cancelled, .ended:
/*
这里有个小问题,转场结束或是取消时有很大几率出现动画不正常的问题.
解决手段是修改交互控制器的 completionSpeed 为1以下的数值,这个属性用来控制动画速度,我猜测是内部实现在边界判断上有问题。
这里其修改为0.99,既解决了 Bug 同时尽可能贴近原来的动画设定.
*/
if progress > 0.3 {
// self.interactionTransition?.completionSpeed = 0.99
//.finish()方法被调用后,转场动画从当前的状态将继续进行直到动画结束,转场完成
self.interactionTransition?.finish()
}else {
//转场取消后,UITabBarController 自动恢复了 selectedIndex 的值,不需要我们手动恢复。
// self.interactionTransition?.completionSpeed = 0.99
//.cancel()被调用后,转场动画从当前的状态回拨到初始状态,转场取消。
self.interactionTransition?.completionSpeed = 0.2
self.interactionTransition?.cancel()
}
//无论转场的结果如何,恢复为非交互状态。
self.interactionTransition = nil
default:
break
}
}
}
extension NavAnimateController: NavigationControllerProtocol, UINavigationControllerDelegate, UIViewControllerAnimatedTransitioning {
var navigationControllerDegate: UINavigationControllerDelegate? {
self
}
/// 返回转场动画代理对象
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.navigationOperationType = operation
let realFrom = (fromVC as? RTContainerController)?.contentViewController ?? fromVC
let realTo = (toVC as? RTContainerController)?.contentViewController ?? toVC
if operation == .push && realTo is Self {
return self
}
if operation == .pop && realFrom is Self {
return self
}
return nil
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactionTransition
}
//
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.8.interval
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//获得容器视图(转场动画发生的地方)
let containerView = transitionContext.containerView
//动画执行时间
let duration = self.transitionDuration(using: transitionContext)
//fromVC (即将消失的视图)
let fromVC = transitionContext.viewController(forKey: .from)!
let fromView = fromVC.view!
//toVC (即将出现的视图)
let toVC = transitionContext.viewController(forKey: .to)!
let toView = toVC.view!
var offset = containerView.frame.height
var fromTransform = CGAffineTransform.identity
var toTransform = CGAffineTransform.identity
offset = navigationOperationType == .push ? -offset : offset
fromTransform = CGAffineTransform(translationX: 0, y: -offset)
toTransform = CGAffineTransform(translationX: 0, y: offset)
containerView.insertSubview(toView, at: 0)
toView.transform = toTransform
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.6, options: .curveEaseInOut) {
fromView.transform = fromTransform
toView.transform = .identity
} completion: { (finished) in
fromView.transform = .identity
toView.transform = .identity
//考虑到转场中途可能取消的情况,转场结束后,恢复视图状态。(通知是否完成转场)
let wasCancelled = transitionContext.transitionWasCancelled
transitionContext.completeTransition(!wasCancelled)
}
}
}
上面演示的效果,我已经实现了 Controller 可以单独定制 navigation 的转场动画,并且可以不影响到其它的 Controller,相当于,Controller 自己管理自己的转场,并且,与业务相当于0耦合.我参考的网上的几篇文章,有的是定制 util 类实现,绝大部分没讲如何封装,如果你对转场的 API 不熟的话,可以参考我下面贴出的两个链接,里面足够的详细
转场的代理逻辑与转场的动画实现,我没有细讲,这东西我下面贴出的引用链接中有讲述,我其实也是参考了他们的代码,我想讲的是,在框架层中,引入新功能时,如何更好地去适配一个项目,当然现在的这个逻辑也不是最终逻辑,有些东西我也纠结了很久,比如
if operation == .push && realTo is Self {
return self
}
if operation == .pop && realFrom is Self {
return self
}
这段判断逻辑到底是给 navigationController 去管理,还是让控制器自己判断,我有点纠结,其实应该是要封装到 navigationController 中,要不然每个需要自定义的 controller 都需要写这样的重复代码,各位看官自行判断.
还有 modal 的转场,十分的简单了,将model 的代理传给 vc 即可,我也不贴路由了,直接用三方的代码
let vc = ModalTransitionViewController()
vc.transitioningDelegate = vc
vc.modalPresentationStyle = .custom
self.present(vc, animated: true, completion: nil)
网友评论