You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在不同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)

其他可选实现方法

  1. 快照+位置参数传递:不直接获取视图引用,而是通过ViewModel传递目标视图的屏幕坐标(如UIScreen.main.bounds内的frame),在动画中创建快照并基于坐标做过渡。
  2. Combine跨ViewModel通信:使用Combine在两个ViewModel间传递动画状态,结合SwiftUI的withAnimation和UIKit的过渡容器,实现跨层级动画同步。
  3. 自定义UIHostingController子类:子类化UIHostingController,重写相关方法来追踪SwiftUI视图的UIKit容器,但此方法依赖SwiftUI内部实现,存在版本兼容风险。

内容的提问来源于stack exchange,提问作者ambassador

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 18:24:54