
一. 引言
在 UIKit 中,自定义转场动画(Custom View Controller Transition)是一套非常强大的能力。它允许我们完全掌控 VC 之间的切换效果,比如:
- 卡片放大过渡
- 列表 Cell → 详情页的共享元素动画
- 模态弹窗的自定义 Present / Dismiss
- 翻页、渐变、覆盖、缩放等任意动画
今天这篇博客我们就来讨论一下,想要实现自定义转场动画的核心,以及一个流程的转场动画的全部实现过程。
二. UIKit自定义转场动画的核心:两个协议
UIKit自定义转场动画有两个核心的协议其中一个负责告诉系统我们想用哪个Animator来接管系统的Present或者是Dismiss动画。而另外一个协议则负责实现这个动画。
2.1 UIViewControllerTransitioningDelegate
UIViewControllerTransitioningDelegate 协议负责告诉系统:在 modal 转场中应该使用哪个 Animator 来做动画。也就是说,它只提供动画对象,不负责实际动画逻辑。
应用场景
- 当一个 VC 的 modalPresentationStyle 设置为 .fullScreen 或 .custom 时,会触发该代理。
- push/pop 场景不适用,push/pop 由 UINavigationControllerDelegate 管理。
核心方法:
Present 动画对象
Swift
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning?
参数说明:
- presented:即将被呈现的 VC,也就是 present(dialog, animated: true) 中的 dialog
- presenting:当前正在呈现 VC 的 VC,也就是执行 present 方法的那个 VC
- source:最初触发 present 的 VC,一般和 presenting 相同,但如果通过嵌套 VC 调用,可能不同
返回值:
返回值是一个遵循 UIViewControllerAnimatedTransitioning 的对象,这个对象负责实际执行动画。
Dismiss 动画对象
Swift
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
参数说明:
- dismissed:即将被关闭的 VC,也就是执行 dismiss(animated: true) 的那个 VC
返回值:
返回值同样是遵循 UIViewControllerAnimatedTransitioning 的对象,负责执行关闭动画。
示例:
Swift
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return GZStartPresentAnimator(fromView: selectedCellStartView,
toView: dialogStartView)
}
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return GZStartDismissAnimator(fromView: dialogStartView,
toView: selectedCellStartView)
}
注意:这两个方法只返回动画对象,本身不会执行动画。动画执行逻辑都写在 UIViewControllerAnimatedTransitioning 中。
2.2 UIViewControllerAnimatedTransitioning
而这个些负责真正的执行动画,遵循这个协议的类需要实现两个代理 方法:
Swift
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)
在该方法内返回动画的时长。
Swift
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
在该方法内实现所有的动画逻辑。
UIKit 会把以下内容交给你:
- containerView:动画的舞台
- fromVC / toVC:参与转场的 VC
- 转场是否取消
- 最终要调用 completeTransition()
三. 从实际代码理解自定义转场流程
下面我们通过一个典型的"列表 Cell 放大 → 模态编辑页"的案例来讲解完整流程。
3.1 第一步:设置 delegate + 传递共享元素
这次的案例中我们采用从外侧传递共享元素到代理,不需要从UIViewControllerTransitioningDelegate的代理方法中获取当前视图控制器和目标视图控制器。
用户点击 cell 后,进入编辑页:
Swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! GZStartCell
let item = list[indexPath.row]
let dialog = GZStartEditorDialog()
dialog.model = item
// 设置模态方式
dialog.modalPresentationStyle = .fullScreen
// 设置自定义转场代理
dialog.transitioningDelegate = GZStartTransitioningDelegate.shared
// ⭐ 关键:把共享元素传给 Transition
GZStartTransitioningDelegate.shared.selectedCellStartView = cell.startView
// ⭐ 同时提前取到"目标视图"
dialog.loadViewIfNeeded()
GZStartTransitioningDelegate.shared.dialogStartView = dialog.startView
present(dialog, animated: true)
}
这段代码做了两件事儿:
- transitioningDelegate 决定由谁提供动画对象
- 共享元素的 fromView、toView 必须提前传递
否则在 animateTransition 中获取不到。
3.2 第二步:实现 TransitioningDelegate,告诉系统用哪个 Animator
创建GZStartTransitioningDelegate 类实现 TransitioningDelegate协议。
Swift
class GZStartTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
static let shared = GZStartTransitioningDelegate()
weak var selectedCellStartView: UIView?
weak var dialogStartView: UIView?
// Present 动画
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return GZStartPresentAnimator(
fromView: selectedCellStartView,
toView: dialogStartView
)
}
// Dismiss 动画
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return GZStartDismissAnimator(
fromView: dialogStartView,
toView: selectedCellStartView
)
}
}
**TransitioningDelegate 的角色 ------**它不做动画!它只是说:
- Present 用哪个 Animator
- Dismiss 用哪个 Animator
动画由后面的 UIViewControllerAnimatedTransitioning 完成。
3.3 第三步:实现 Present 动画(核心)
下面以 GZStartPresentAnimator 为例------这段代码是你现在项目中真正使用的。
3.3.1 定义GZStartPresentAnimator类型
定义GZStartPresentAnimator类型 并遵循 UIViewControllerAnimatedTransitioning协议,实现动画时长的协议方法。
Swift
class GZStartPresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let fromView: UIView?
let toView: UIView?
init(fromView: UIView?, toView: UIView?) {
self.fromView = fromView
self.toView = toView
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)
-> TimeInterval { 0.35 }
}
3.3.2 开始动画逻辑
在animateTransition 方法中实现动画逻辑
Swift
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromView = fromView,
let toView = toView,
let toVC = transitionContext.viewController(forKey: .to)
else {
transitionContext.completeTransition(true)
return
}
let container = transitionContext.containerView
}
在该方法内我们获取了需要做动画的目标实体,以及动画的容器视图。
3.3.3 创建 snapshot(共享元素动画的关键)
Swift
let snapshot = fromView.snapshotView(afterScreenUpdates: false)
?? fromView.snapshotView(afterScreenUpdates: true)
guard let snapshotView = snapshot else {
transitionContext.completeTransition(true)
return
}
为什么 snapshot 必须两次?
因为某些情况下 afterScreenUpdates = false 会失败。
3.3.4 把目标 VC 放进 container
Swift
let finalFrame = transitionContext.finalFrame(for: toVC)
toVC.view.frame = finalFrame
toVC.view.alpha = 0
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
layoutIfNeeded 非常重要否则目标 view 的布局尚未确定,计算目标位置时 frame 会是 0。
3.3.5 把 snapshot 加进来(动画主角)
Swift
let startFrame = fromView.convert(fromView.bounds, to: container)
snapshotView.frame = startFrame
container.addSubview(snapshotView)
3.3.6 计算目标位置(共享元素的终点)
Swift
let endFrame = toView.convert(toView.bounds, to: container)
3.3.7 动画执行
Swift
let duration = transitionDuration(using: transitionContext)
UIView.animate(
withDuration: duration,
delay: 0,
options: [.curveEaseInOut, .allowUserInteraction],
animations: {
snapshotView.frame = endFrame
toVC.view.alpha = 1
},
completion: { _ in
let wasCancelled = transitionContext.transitionWasCancelled
snapshotView.removeFromSuperview()
if wasCancelled {
toVC.view.removeFromSuperview()
}
transitionContext.completeTransition(!wasCancelled)
print("动画完成")
}
)
这里我们做了两件事:
- snapshot 从 from → to 的位置动画
- toVC 从 alpha 0 → 1 渐显
四. 结语
过本文,我们完整梳理了 UIKit 自定义转场动画的核心原理和实现流程:
核心协议
- UIViewControllerTransitioningDelegate:负责告诉系统使用哪个 Animator 来执行 Present/Dismiss 动画。
- UIViewControllerAnimatedTransitioning:负责真正执行动画逻辑,包括 snapshot、frame 计算和最终动画。
动画实现流程
- 获取 containerView 作为动画舞台
- 获取 fromView / toView 并创建 snapshot
- 把目标 VC 放入 container 并计算终点位置
- 执行动画,最后调用 completeTransition 完成转场
理论与实践结合
- 理解了这两个协议后,你就可以实现各种自定义动画:共享元素过渡、卡片放大、弹窗渐入等。
- 注意 modal 转场与 push/pop 转场的区别:push/pop 需要通过 UINavigationControllerDelegate 提供动画对象,而不是 transitioningDelegate。
UIKit 自定义转场看似复杂,但只要理解了"谁提供动画对象 "和"动画逻辑在哪里执行",整个流程就非常清晰。
下一步,你可以尝试将同样的逻辑应用到 push/pop 场景,或者加入交互式手势,让动画更生动灵活。