SwiftUI视图尺寸变化时,如何为UIHostingController添加尺寸动画?
解决UIHostingController与SwiftUI视图动画尺寸不同步问题
核心问题在于SwiftUI的动画系统和UIKit的布局系统相互独立,SwiftUI内部视图的动画不会自动触发UIHostingController的尺寸过渡动画。下面提供两种可行的解决方案:
方案一:通过PreferenceKey传递尺寸并触发UIKit动画
1. 定义尺寸传递的PreferenceKey
struct ContentSizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } } // 辅助扩展:监听视图实时尺寸变化 extension View { func readSize(onChange: @escaping (CGSize) -> Void) -> some View { background( GeometryReader { proxy in Color.clear .preference(key: SizePreferenceKey.self, value: proxy.size) } ) .onPreferenceChange(SizePreferenceKey.self, perform: onChange) } } private struct SizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } }
2. 在SwiftUI视图中传递尺寸
struct CollapsibleContentView: View { @State private var isExpanded = false var onSizeChange: ((CGSize) -> Void)? var body: some View { VStack(spacing: 16) { Button("展开/折叠") { withAnimation(.easeInOut(duration: 0.4)) { isExpanded.toggle() } } .padding(.top, 8) if isExpanded { VStack { Text("这是展开后的内容区域") Text("多行文本用来测试高度变化") Text("确保动画过程中尺寸平滑过渡") } .padding(.bottom, 8) } } .background(Color.gray.opacity(0.1)) .readSize { size in onSizeChange?(size) } } }
3. 在UIKit宿主控制器中监听并动画更新
class CollapsibleHostVC: UIViewController { private var heightConstraint: NSLayoutConstraint! override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white let swiftUIView = CollapsibleContentView { [weak self] newSize in guard let self = self else { return } // 同步更新高度约束并触发UIKit动画 UIView.animate(withDuration: 0.4) { self.heightConstraint.constant = newSize.height self.view.layoutIfNeeded() } } let hostingVC = UIHostingController(rootView: swiftUIView) addChild(hostingVC) view.addSubview(hostingVC.view) hostingVC.didMove(toParent: self) // 设置约束 hostingVC.view.translatesAutoresizingMaskIntoConstraints = false let constraints = [ hostingVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), hostingVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20) ] NSLayoutConstraint.activate(constraints) // 绑定高度约束,初始值为SwiftUI视图的intrinsic尺寸 heightConstraint = hostingVC.view.heightAnchor.constraint(equalToConstant: hostingVC.view.intrinsicContentSize.height) heightConstraint.isActive = true } }
方案二:自定义AnimatedHostingController子类
通过重写intrinsicContentSize,在尺寸变化时自动触发UIKit动画:
class AnimatedHostingController<Content: View>: UIHostingController<Content> { private var lastIntrinsicSize: CGSize = .zero override var intrinsicContentSize: CGSize { let newSize = super.intrinsicContentSize // 仅当尺寸实际变化时触发动画 guard newSize != lastIntrinsicSize else { return newSize } lastIntrinsicSize = newSize // 通知父视图布局需要更新,并执行动画 invalidateIntrinsicContentSize() DispatchQueue.main.async { UIView.animate(withDuration: 0.4) { self.view.superview?.layoutIfNeeded() } } return newSize } }
使用方式
在UIKit中直接使用这个子类代替原生UIHostingController,并确保父视图的约束依赖于intrinsicContentSize:
class DemoVC: UIViewController { override func viewDidLoad() { super.viewDidLoad() let hostingVC = AnimatedHostingController(rootView: CollapsibleContentView()) addChild(hostingVC) view.addSubview(hostingVC.view) hostingVC.didMove(toParent: self) hostingVC.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ hostingVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), hostingVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), // 高度由intrinsicContentSize自动决定 hostingVC.view.heightAnchor.constraint(equalToConstant: hostingVC.view.intrinsicContentSize.height) ]) } }
注意事项
- 确保SwiftUI动画时长与UIKit动画时长一致,避免出现不同步的情况
- 所有布局操作必须在主线程执行,避免线程安全问题
- 如果使用约束方式,务必正确持有高度约束的引用,防止找不到约束导致动画失效
内容的提问来源于stack exchange,提问作者Adam Carter
相关产品推荐
相关产品推荐

