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

SwiftUI中如何控制封装的UIScrollView滚动到指定位置

问题描述

我通过UIKit实现了UIScrollView并封装为SwiftUI可用的组件,需要实现两类滚动控制需求:

  • 点击SwiftUI视图中的按钮时,控制封装的UIScrollView滚动到指定位置
  • UIScrollView首次展示时自动滚动到指定位置

我尝试直接设置contentOffset未生效,无法定位问题原因。

现有代码

UIScrollView封装代码

class UIScrollViewViewController: UIViewController {
    lazy var scrollView: UIScrollView = {
        let v = UIScrollView()
        v.isPagingEnabled = false
        v.alwaysBounceVertical = true
        return v
    }()

    var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.scrollView)
        self.pinEdges(of: self.scrollView, to: self.view)

        self.hostingController.willMove(toParent: self)
        self.scrollView.addSubview(self.hostingController.view)
        self.pinEdges(of: self.hostingController.view, to: self.scrollView)
        self.hostingController.didMove(toParent: self)

    }

    func pinEdges(of viewA: UIView, to viewB: UIView) {
        viewA.translatesAutoresizingMaskIntoConstraints = false
        viewB.addConstraints([
        viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
        viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
        viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
        viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
        ])
    }

}

struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {

    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> UIScrollViewViewController {
        let vc = UIScrollViewViewController()
        vc.hostingController.rootView = AnyView(self.content())
        return vc
    }

    func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
        viewController.hostingController.rootView = AnyView(self.content())
    }
}

SwiftUI侧使用示例

struct ContentView: View{
    @ObservedObject var search = SearchBar()
    var body: some View{
       NavigationView{
        GeometryReader{geo in
            UIScrollViewWrapper{
                VStack{
                    ForEach(0..<10){i in
                        Text("lskdfj")
                    }
                }
                .frame(width: geo.size.width)
            }
            .navigationBarTitle("Test")
        }
       }
    }
}
解决方案

contentOffset设置不生效的核心原因有两个:一是原有封装没有暴露偏移量控制的双向绑定入口,二是直接在布局未完成的生命周期节点设置偏移,此时scrollView的contentSize还未计算完成,偏移设置不会生效。

修改后的完整实现如下:

调整后的UIScrollView封装代码

struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
    // 双向绑定偏移量参数
    @Binding var contentOffset: CGPoint
    // 标记首次滚动执行状态,避免重复触发
    @State private var hasInitialScrolled = false
    var content: () -> Content

    init(contentOffset: Binding<CGPoint>, @ViewBuilder content: @escaping () -> Content) {
        self._contentOffset = contentOffset
        self.content = content
    }

    func makeUIViewController(context: Context) -> UIScrollViewViewController {
        let vc = UIScrollViewViewController()
        vc.hostingController.rootView = AnyView(self.content())
        return vc
    }

    func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
        viewController.hostingController.rootView = AnyView(self.content())
        let scrollView = viewController.scrollView
        // 先触发布局更新,确保contentSize计算完成
        viewController.view.layoutIfNeeded()
        
        // 处理首次加载自动滚动
        if !hasInitialScrolled {
            DispatchQueue.main.async {
                scrollView.setContentOffset(contentOffset, animated: false)
                hasInitialScrolled = true
            }
            return
        }
        
        // 处理外部触发的滚动需求
        if scrollView.contentOffset != contentOffset {
            scrollView.setContentOffset(contentOffset, animated: true)
        }
    }
}

调整后的SwiftUI使用示例

struct ContentView: View{
    // 状态变量控制滚动偏移,初始值即为首次加载要自动滚动到的位置
    @State private var scrollOffset = CGPoint(x: 0, y: 200)
    @ObservedObject var search = SearchBar()
    var body: some View{
       NavigationView{
           VStack(spacing: 12) {
               // 滚动控制按钮
               HStack(spacing: 20) {
                   Button("滚动到顶部") {
                       scrollOffset = .zero
                   }
                   Button("滚动到y=400位置") {
                       scrollOffset = CGPoint(x: 0, y: 400)
                   }
               }
               GeometryReader{geo in
                   UIScrollViewWrapper(contentOffset: $scrollOffset){
                       VStack(spacing: 0){
                           ForEach(0..<10){i in
                               Text("第\(i)行内容")
                                   .frame(height: 80)
                                   .frame(maxWidth: .infinity)
                                   .background(i%2 == 0 ? .gray.opacity(0.2) : .white)
                           }
                       }
                       .frame(width: geo.size.width)
                   }
                   .navigationBarTitle("Test")
               }
           }
           .padding(.top, 12)
       }
    }
}

补充说明

如果需要把UIScrollView的实时滚动偏移同步回SwiftUI侧,只需要在UIScrollViewViewController中实现UIScrollViewDelegate的scrollViewDidScroll方法,通过Coordinator回调更新绑定的contentOffset参数即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 18:21:03