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

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)
        ])
    }
}

问题根因

原有实现存在三个核心错误,直接导致动画丢失:

  1. 在值类型的UIViewRepresentable结构体中持有UIKit对象实例:UIViewRepresentable会随SwiftUI状态更新多次重新创建,持有UIScrollView、UIHostingController会导致对象生命周期混乱,状态更新时无法正确关联到原有视图实例
  2. updateUIView中重复执行添加子视图、添加约束的逻辑:每次状态更新都会重复挂载host.view、新增重复约束,造成布局冲突,直接打断SwiftUI动画事务的传递
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:27:25