美文网首页
iOS(Swift) 转场动画的框架设计

iOS(Swift) 转场动画的框架设计

作者: 简单coder | 来源:发表于2021-07-13 23:23 被阅读0次

本篇介绍下转场动画的定制化,转场动画用的不多,但是还是会有需要在某些页面特地定制化的一些场景出现,今天抽了些时间,把框架理了一下,做成了可让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 的转场动画.

具体实现效果如下图

最后贴出一个完整的业务控制器代码,这里面我也顺便实现了交互的转场


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)

参考文章:
iOS-Swift转场动画详解
iOS 自定义页面的切换动画与交互动画 By Swift

相关文章

网友评论

      本文标题:iOS(Swift) 转场动画的框架设计

      本文链接:https://www.haomeiwen.com/subject/rwxepltx.html