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
相关产品推荐
相关产品推荐

