转场动画,就是Vc切换过程中的过渡动画。
官方支持以下几种方式的自定义转场:
- UINavigationController 中 push 和 pop;
- Modal 转场:present 和 dismiss;
如果需要更定制化的动画就需要自定义设计转场了
本文主要简单记录分享下自定义转场动画的实现方法, 包含卡片转场动画,开关门动画,手势转场等,代码可以到Github TransitionAnimationTest下载查看
转场动画对象
-
创建转场动画控制对象 继承UIViewControllerAnimatedTransitioning对象
实现以下两个代理方法
/// 返回动画时长 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.25 } /// 所有的过渡动画事务都在这个方法里面完成 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { switch self.type { case .push: self.pushAnimation(transitionContext: transitionContext) case .pop: self.popAnimation(transitionContext: transitionContext) case .present: self.presentAnimation(transitionContext: transitionContext) case .dismiss: self.dismissAnimation(transitionContext: transitionContext) } }
-
转场环境协议
转场环境协议 UIViewControllerContextTransitioning 是转场动画的核心,存储转场双方的数据等
动画协议UIViewControllerAnimatedTransitioning 的两个代理方法都有个遵守UIViewControllerContextTransitioning协议的参数,表示的是当前转场的上下文,可以获取到 fromViewController、或者是toViewController以及fromView、toView、containerView等。
// 通过viewControllerForKey取出转场前后的两个控制器,这里toVC就是转场后的VC、fromVC就是转场前的VC let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) var fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) // 取出转场前后视图控制器上的视图view let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
经过试验发现不同情况下的fromView、toView有不同的表现
-
push
可以发现fromView、toView都是有值的,且fromView与fromVC的view一致,toView与toVC的view一致
-
pop
同上
-
present
fromView为nil
-
dismiss
toView为nil
-
-
containerView
如果要对视图做转场动画,视图就必须要加入containerView中才能进行,可以理解containerView管理着所有做转场动画的视图
let containerView = transitionContext.containerView containerView.addSubview(toVC.view) // 一般使用snapshotView制作截屏来实现动画效果 let tempView = animationView.snapshotView(afterScreenUpdates: false) containerView.addSubview(tempView)
注意:push和pop以及present的时候,需要添加toVC.view到容器视图上,才能完成正常跳转, dismiss的时候则不需要添加,否则会出现黑屏问题
-
动画完成之后需要执行完成方法
// 当有手势参与时,此处需要进行手势过渡判断,没有则可以直接传true transitionContext.completeTransition(true)
导航控制器push和pop 自定义转场
创建转场动画对象
-
UINavigationControllerDelegate代理
push和pop转场需要通过UINavigationControllerDelegate来实现
需要跳转的控制器需要继承UINavigationControllerDelegate,并实现以下方法
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { // 在此处返回转场动画控制对象 return self.transitionAnimation }
-
delegate生命周期
-
push前
记录原代理对象
(vc as? ViewControllerOne)?.navDelegate = self.navigationController?.delegate
为转场到的控制器设置代理,未设置会执行系统转场动画
self.navigationController?.delegate = vc as? UINavigationControllerDelegate self.navigationController?.pushViewController(vc, animated: true)
-
pop后
还原代理,否则会出现内存泄漏导致crash问题
研究发现过早还原会导致转场动画不生效,太晚还原会导致crash
网上查询到的资料都建议在viewDidDisappear进行还原,但实际验证发现此时navigationController已经为nil,无法设置代理,无效操作
// 无效 override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.navigationController?.delegate = self.navDelegate }
研究发现可以在UINavigationControllerDelegate的另外一个代理方法中执行还原操作
/// 发生转场,新的控制器界面即将出现 func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { // 返回到上级界面时才进行还原代理操作 guard viewController != self else { return } guard let delegate = self.navigationController?.delegate as? UIViewController, delegate == self else { return } // 还原代理 self.navigationController?.delegate = self.navDelegate }
-
当前界面继续进行转场
当前界面需要继续进行转场时,需要先还原代理,否则会出现转场异常,导致黑屏等问题
@objc func jumpVC() { let vc = ViewController() self.navigationController?.delegate = self.navDelegate // 跳转之前需要先还原代理 self.navigationController?.pushViewController(vc, animated: true) }
从别的界面返回到当前界面,需要重设代理为自身,不然返回时会执行系统转场动画
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 由其他界面返回到当前界面时 设置代理 self.navigationController?.delegate = self }
-
模态present和dismiss 自定义转场
创建转场动画对象
-
UIViewControllerTransitioningDelegate代理
present和dismiss转场需要通过UIViewControllerTransitioningDelegate来实现
需要跳转的控制器需要继承UIViewControllerTransitioningDelegate,并实现以下方法
/// present转场动画 func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.transitionAnimation.type = .present return self.transitionAnimation } /// dismiss转场动画 func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.transitionAnimation.type = .dismiss return self.transitionAnimation }
-
delegate生命周期
界面创建时设置代理
self.transitioningDelegate = self self.modalPresentationStyle = .custom
手势转场
创建转场动画对象
-
创建手势转场控制对象
继承自交互控制器协议 UIViewControllerInteractiveTransitioning, 官方提供了一个已实现UIViewControllerInteractiveTransitioning协议的类UIPercentDrivenInteractiveTransition 为我们预先实现和提供了一系列便利的方法,可以用一个百分比来控制交互式切换的过程,利用手势来完成这个转场
//暂停交互 - (void)pauseInteractiveTransition NS_AVAILABLE_IOS(10_0); //更新方法,一般交互时候的进度更新就在这个方法里面 - (void)updateInteractiveTransition:(CGFloat)percentComplete; //第三个是取消交互 - (void)cancelInteractiveTransition; //第四个的话就是设置交互完成 - (void)finishInteractiveTransition;
-
为转场控制器添加手势
/// 添加手势 func addPanGesture(vc: UIViewController) { self.transitionVC = vc let pan = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(gesture:))) vc.view.addGestureRecognizer(pan) }
针对手势操作进行处理,通过UIPercentDrivenInteractiveTransition的方法来控制进度
fileprivate func interactiveWithPop(gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: gesture.view) guard let transitionVC = self.transitionVC else { return } // 获取百分比 var percentComplete: CGFloat = 0 percentComplete = abs(translation.x / transitionVC.view.frame.size.width) // 根据手势状态进行处理 switch gesture.state { case .began: // 开始转场动画 self.isInteractive = true transitionVC.navigationController?.popViewController(animated: true) case .changed: // 手势过程中,通过updateInteractiveTransition设置转场过程动画进行的百分比,然后系统会根据百分比自动布局动画控件,不用我们控制了 self.update(percentComplete) case .ended: // 根据进度来结束转场或取消转场 self.isInteractive = false if percentComplete > 0.5 { self.finish() } else { self.cancel() } default: break } }
-
push和pop 手势转场
实现UINavigationControllerDelegate的代理方法
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { switch self.transitionAnimation.type { case .pop: // 如果不是通过手势触发则返回nil return self.transitionInteractive.isInteractive ? self.transitionInteractive : nil default: return nil } }
同时,要在转场动画对象中添加手势判断
决定是否完成转场,抑或取消转场
/// pop动效 fileprivate func popAnimation(transitionContext: UIViewControllerContextTransitioning) { ………… UIView.animate(withDuration: self.transitionDuration(using: transitionContext)) {…………} completion: { (isFinished) in ………… // 由于加入了手势必须判断 transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } }
-
present、dismiss手势转场
实现UIViewControllerTransitioningDelegate的代理方法
/// 控制present手势 func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return nil } /// 控制dismiss手势 func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return self.transitionInteractive }
同时,要在转场动画对象中添加手势判断,同上4
仿系统转场动画
另外可以通过自定义动画,实现仿系统转场
本身还是使用正常的导航控制器转场或者模态转场;
将转场时的animated参数置为NO,在新的控制器将要出现或需要返回时在界面内创建动画效果