SwiftUI中ScrollView传ScrollViewReader到子视图滚动偏移如何解决
问题原因
该问题属于Xcode 13 Beta 5版本中SwiftUI的已知布局bug:当ScrollViewProxy被传递到ScrollViewReader闭包以外的子视图使用时,系统计算滚动锚点时会错误统计容器安全区域、内容尺寸的上下文,导致异常偏移。如果ForEach与ScrollViewReader处于同一视图层级,布局计算共享同一上下文,就不会触发该问题。
解决方案
方案1:添加布局约束修正偏移(改动最小)
仅需要给ScrollView的根内容容器添加满宽约束,即可修正布局计算错误,修改后的代码如下:
struct ContentView: View { var body: some View { VStack { ScrollView { ScrollViewReader { proxy in // 把所有内容包在同一个VStack里,添加满宽约束 VStack { Button("Scroll to bottom") { withAnimation { proxy.scrollTo(99, anchor: .bottom) } } TestView(proxy: proxy) } .frame(maxWidth: .infinity) // 新增这一行 } } } } } struct TestView: View { var proxy: ScrollViewProxy var body: some View { VStack { ForEach(1..<100) { index in Text("Test \(index)") .id(index) } Button("Scroll to top") { withAnimation { proxy.scrollTo(1, anchor: .top) } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
方案2:收敛ScrollViewProxy使用范围(兼容性最好)
不要将ScrollViewProxy传递出ScrollViewReader的闭包范围,通过状态变量传递滚动需求,避免跨上下文的布局计算错误:
struct ContentView: View { // 声明滚动目标状态 @State private var scrollTarget: Int? var body: some View { VStack { ScrollView { ScrollViewReader { proxy in VStack { Button("Scroll to bottom") { scrollTarget = 99 } TestView(onScrollToTop: { scrollTarget = 1 }) } .frame(maxWidth: .infinity) // 监听滚动目标变化,在当前上下文执行滚动 .onChange(of: scrollTarget) { target in guard let target = target else { return } withAnimation { if target == 1 { proxy.scrollTo(target, anchor: .top) } else { proxy.scrollTo(target, anchor: .bottom) } } // 重置状态避免重复触发 scrollTarget = nil } } } } } } struct TestView: View { // 回调传递滚动需求,不用传入proxy var onScrollToTop: () -> Void var body: some View { VStack { ForEach(1..<100) { index in Text("Test \(index)") .id(index) } Button("Scroll to top", action: onScrollToTop) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
方案3:升级Xcode版本
该bug在Xcode 13正式版及后续版本中已经被官方修复,升级开发环境到正式版后,原有代码不需要改动也可以解决偏移问题。
内容的提问来源于stack exchange,提问作者runemonster
相关产品推荐
相关产品推荐

