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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 20:53:15