如何在不同UIHostingController间实现SwiftUI视图过渡动画
不同UIHostingController间子视图的跨层级过渡动画实现方案
问题场景
使用两个承载SwiftUI视图的UIHostingController,通过.present(...)模态展示其中一个;需要在dismiss模态控制器时,让两个UIHostingController根视图层级内的特定子视图实现帧过渡动画(即一个视图的帧平滑过渡到另一个视图的帧)。
现有方案的局限
- 纯SwiftUI方案:
matchedGeometryEffect可轻松实现同视图树内的视图过渡,但由于两个视图分属不同UIHostingController,不在同一SwiftUI视图树中,此方案无效。 - 纯UIKit方案:通过实现
UIViewControllerAnimatedTransitioning和UIViewControllerTransitioningDelegate可完成自定义过渡,但动画需要获取目标子视图的UIView实例及其帧信息;直接通过view.subviews遍历无法拿到SwiftUI子视图对应的UIView(SwiftUI视图在UIKit层级中是封装后的内部容器,无法直接通过层级遍历访问)。
核心解决方案:将SwiftUI子视图暴露给UIKit
可以通过自定义SwiftUI修饰符,将目标子视图对应的UIView实例引用传递到UIKit层(如ViewModel或ViewController),具体步骤如下:
1. 定义视图引用存储载体
在ViewModel中添加变量存储目标视图的UIView引用:
class MyViewModel: ObservableObject { // 存储目标SwiftUI子视图对应的UIView实例 var targetViewRef: UIView? = nil }
2. 实现自定义视图引用修饰符
创建可传递UIView引用的SwiftUI修饰符,通过UIViewRepresentable获取并传递视图引用:
// 自定义修饰符:将SwiftUI子视图的UIView引用传递到外部 struct UIViewReferenceModifier: ViewModifier { let ref: Binding<UIView?> func body(content: Content) -> some View { // 添加透明的UIViewRepresentable作为背景,通过其superview获取目标视图的UIView实例 content.background(UIViewRepresentableRef(ref: ref)) } } // 辅助UIViewRepresentable:用于获取父视图引用 struct UIViewRepresentableRef: UIViewRepresentable { let ref: Binding<UIView?> func makeUIView(context: Context) -> UIView { let view = UIView() view.isHidden = true // 隐藏辅助视图,不影响界面 view.backgroundColor = .clear return view } func updateUIView(_ uiView: UIView, context: Context) { // 获取辅助视图的父视图(即目标SwiftUI子视图对应的UIKit容器) ref.wrappedValue = uiView.superview?.superview } } // 扩展View,方便调用修饰符 extension View { func uiViewReference(_ ref: Binding<UIView?>) -> some View { modifier(UIViewReferenceModifier(ref: ref)) } }
3. 在SwiftUI目标子视图上应用修饰符
在需要过渡的SwiftUI子视图上添加上述修饰符,将视图引用绑定到ViewModel:
struct MyView: View { @ObservedObject var viewModel: MyViewModel var body: some View { // 示例:需要过渡的图片视图 Image("target-image") .resizable() .frame(width: 80, height: 80) // 应用修饰符,传递视图引用到ViewModel .uiViewReference($viewModel.targetViewRef) } }
4. 在UIKit自定义过渡中使用视图引用
修改自定义过渡动画控制器,直接通过ViewModel获取目标视图的UIView实例:
class ShrinkDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.6 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from) as? MyViewController, let toVC = transitionContext.viewController(forKey: .to) as? MyViewController, let fromTargetView = fromVC.viewModel.targetViewRef, let toTargetView = toVC.viewModel.targetViewRef else { transitionContext.completeTransition(false) return } let containerView = transitionContext.containerView // 从模态视图的目标视图创建快照,避免原视图被隐藏影响动画 let snapshot = fromTargetView.snapshotView(afterScreenUpdates: true)! snapshot.frame = containerView.convert(fromTargetView.frame, from: fromTargetView.superview) containerView.addSubview(snapshot) // 隐藏原视图,避免动画过程中显示重复内容 fromTargetView.isHidden = true toTargetView.isHidden = true // 获取目标视图在容器中的最终帧 let targetFrame = containerView.convert(toTargetView.frame, from: toTargetView.superview) // 执行帧过渡动画 UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseInOut, animations: { snapshot.frame = targetFrame }) { finished in // 动画完成后恢复原视图显示,并清理快照 fromTargetView.isHidden = false toTargetView.isHidden = false snapshot.removeFromSuperview() transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } } class ShrinkDismissTransition: NSObject, UIViewControllerTransitioningDelegate { func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { guard dismissed is MyViewController else { return nil } return ShrinkDismissAnimationController() } }
5. 配置模态过渡
在present模态控制器时,设置自定义过渡代理:
// 创建并配置模态视图控制器 let modalVC = MyViewController() modalVC.viewModel = MyViewModel() modalVC.modalPresentationStyle = .fullScreen // 设置自定义过渡代理 let transitionDelegate = ShrinkDismissTransition() modalVC.transitioningDelegate = transitionDelegate // 执行present navigationController?.present(modalVC, animated: true)
其他可选实现方法
- 快照+位置参数传递:不直接获取视图引用,而是通过ViewModel传递目标视图的屏幕坐标(如
UIScreen.main.bounds内的frame),在动画中创建快照并基于坐标做过渡。 - Combine跨ViewModel通信:使用Combine在两个ViewModel间传递动画状态,结合SwiftUI的
withAnimation和UIKit的过渡容器,实现跨层级动画同步。 - 自定义UIHostingController子类:子类化
UIHostingController,重写相关方法来追踪SwiftUI视图的UIKit容器,但此方法依赖SwiftUI内部实现,存在版本兼容风险。
内容的提问来源于stack exchange,提问作者ambassador
相关产品推荐
相关产品推荐

