SwiftUI中UIViewRepresentable包装UIScrollView时withAnimation失效
问题:UIViewRepresentable封装的UIScrollView无法响应withAnimation动画
问题现象
通过UIViewRepresentable桥接UIKit的UIScrollView时,滚动视图内部的SwiftUI内容无法响应withAnimation触发的动画,状态变化会直接跳变;同代码放在SwiftUI原生ScrollView中动画可正常执行。
复现代码
测试页面代码
import SwiftUI import UIKit struct FriendView: View { @State var display = false var body: some View { VStack { // UIViewRepresentable + UIKit 实现的滚动视图 UScrollView { VStack { RoundedRectangle(cornerRadius: 10) .fill(self.display ? .red.opacity(0.3) : .blue.opacity(0.3)) .frame(width: 200, height: self.display ? 200 : 100) .onTapGesture { withAnimation { self.display.toggle() } } } } // SwiftUI 原生滚动视图 ScrollView { VStack { RoundedRectangle(cornerRadius: 10) .fill(self.display ? .red.opacity(0.3) : .blue.opacity(0.3)) .frame(width: 200, height: self.display ? 200 : 100) .onTapGesture { withAnimation { self.display.toggle() } } } } } } }
原有桥接实现代码
struct UScrollView<Content: View>: UIViewRepresentable { typealias UIViewType = UIScrollView var content: () -> Content let scrollView: UIScrollView = { let view = UIScrollView() return view }() var host: UIHostingController<AnyView> = UIHostingController( rootView: AnyView(EmptyView()) ) init(@ViewBuilder content: @escaping () -> Content) { self.content = content } func makeUIView(context: Context) -> UIScrollView { host.rootView = AnyView(self.content()) host.view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(host.view) scrollView.addConstraints([ host.view.topAnchor.constraint(equalTo: scrollView.topAnchor), host.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), host.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), host.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), host.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor) ]) return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { host.rootView = AnyView(self.content()) host.view.translatesAutoresizingMaskIntoConstraints = false uiView.addSubview(host.view) uiView.addConstraints([ host.view.topAnchor.constraint(equalTo: uiView.topAnchor), host.view.leadingAnchor.constraint(equalTo: uiView.leadingAnchor), host.view.trailingAnchor.constraint(equalTo: uiView.trailingAnchor), host.view.bottomAnchor.constraint(equalTo: uiView.bottomAnchor), host.view.widthAnchor.constraint(equalTo: uiView.widthAnchor) ]) } }
问题根因
原有实现存在三个核心错误,直接导致动画丢失:
- 在值类型的
UIViewRepresentable结构体中持有UIKit对象实例:UIViewRepresentable会随SwiftUI状态更新多次重新创建,持有UIScrollView、UIHostingController会导致对象生命周期混乱,状态更新时无法正确关联到原有视图实例 updateUIView中重复执行添加子视图、添加约束的逻辑:每次状态更新都会重复挂载host.view、新增重复约束,造成布局冲突,直接打断SwiftUI动画事务的传递UIHostingController未加入视图控制器层级:仅把host.view加为UIScrollView的子视图,没有将UIHostingController作为子控制器挂载到当前响应链的父控制器上,会导致SwiftUI的动画上下文、环境值在跨UIKit桥接边界时丢失,withAnimation触发的动画事务无法传递到托管的SwiftUI内容中
修复方案
修正后的桥接实现如下:
struct UScrollView<Content: View>: UIViewRepresentable { typealias UIViewType = UIScrollView var content: () -> Content init(@ViewBuilder content: @escaping () -> Content) { self.content = content } func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = true guard let parentVC = context.coordinator.findCurrentParentViewController() else { return scrollView } // 初始化HostingController,加入控制器层级 let hostVC = UIHostingController(rootView: AnyView(content())) parentVC.addChild(hostVC) hostVC.view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(hostVC.view) hostVC.didMove(toParent: parentVC) // 约束仅添加一次,绑定到UIScrollView的标准LayoutGuide NSLayoutConstraint.activate([ hostVC.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), hostVC.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), hostVC.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), hostVC.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), hostVC.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) ]) context.coordinator.hostVC = hostVC return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { // 仅更新托管的SwiftUI内容,不做重复的视图添加、约束添加操作 context.coordinator.hostVC?.rootView = AnyView(content()) } func makeCoordinator() -> Coordinator { Coordinator() } class Coordinator { var hostVC: UIHostingController<AnyView>? func findCurrentParentViewController() -> UIViewController? { // 沿响应链递归查找当前所在的UIViewController guard let windowScene = UIApplication.shared.connectedScenes .filter({ $0.activationState == .foregroundActive }) .compactMap({ $0 as? UIWindowScene }) .first, let keyWindow = windowScene.windows.first(where: \.isKeyWindow), let rootVC = keyWindow.rootViewController else { return nil } return findResponderViewController(from: rootVC.next) } private func findResponderViewController(from responder: UIResponder?) -> UIViewController? { guard let responder = responder else { return nil } if let viewController = responder as? UIViewController { return viewController } return findResponderViewController(from: responder.next) } } }
修复要点说明
- 所有UIKit对象的引用统一放在
Coordinator中持有,Coordinator是UIKit桥接对象的上下文持有类,生命周期和桥接的UIView一致,不会随UIViewRepresentable值类型的重建而销毁 - 子视图添加、约束绑定逻辑仅在
makeUIView阶段执行一次,updateUIView只负责同步UI状态、更新托管的SwiftUI根视图,避免重复约束造成布局冲突 - 严格遵循UIKit控制器容器规范,将
UIHostingController作为子控制器挂载到当前响应链的父控制器上,保证SwiftUI的动画事务、环境对象可以正常传递到托管内容中 - 约束绑定到
UIScrollView的contentLayoutGuide和frameLayoutGuide,符合UIScrollView的AutoLayout标准用法,避免内容大小计算异常
内容的提问来源于stack exchange,提问作者chenshuo
相关产品推荐
相关产品推荐

