为何ScrollView在真机/模拟器中随机变内容高度,预览却正常?
问题描述
我当前使用Xcode Version 14.3.1 (14E300c)。代码中的ScrollView通过ForEach遍历对象数组,想要实现用户切换视图后回到精确滚动位置(类似社交应用的行为)。尝试用ScrollViewReader.scrollTo(<id: , anchor:>)实现,仅在预览中正常,在模拟器或iPhone 14真机中,ScrollView内容高度会随机变化。该问题仅在滚动至内容接近底部时出现,滚动至顶部附近则恢复正常。
原始代码
// 疑似引发问题的视图 struct ScrollViewWithSavedPosition: View { @ObservedObject var scrollViewSavedValue: ScrollViewSavedValue @State private var isViewLoaded: Bool = false let geoProxy: GeometryProxy let maxScrollableHeight: CGFloat = 4809 var body: some View { ScrollViewReader { scrollProxy in ScrollView(showsIndicators: false) { LazyVStack { ForEach(0..<34) { poster in ZStack { Rectangle() .fill(.black) .frame(width: geoProxy.size.width * 0.4, height: geoProxy.size.height * 0.2) Text("\(poster)") .foregroundColor(.orange) } } } .id("scrollPosition") .background( GeometryReader { Color.orange .preference(key: CGPointPK2.self, value: $0.frame(in: .global).origin) } ) .onPreferenceChange(CGPointPK2.self) { scrollPosition in DispatchQueue.main.async { isViewLoaded = true } if isViewLoaded { let offsetValue = (-1 * (scrollPosition.y - geoProxy.safeAreaInsets.top)) / maxScrollableHeight scrollViewSavedValue.scrollOffsetValue = offsetValue } print(scrollViewSavedValue.scrollOffsetValue) } } .onAppear { scrollProxy.scrollTo("scrollPosition", anchor: UnitPoint(x: 0, y: scrollViewSavedValue.scrollOffsetValue)) } } .preferredColorScheme(.light) } } struct CGPointPK2: PreferenceKey { static var defaultValue: CGPoint = .zero static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { } } class ScrollViewSavedValue: ObservableObject { @Published var scrollOffsetValue: CGFloat = 0 @Published var selectedTab: Int = 1 } struct TabBarView: View { @ObservedObject var scrollViewSavedValue: ScrollViewSavedValue let geoProxy: GeometryProxy var body: some View { VStack { Spacer() ZStack { Rectangle() .fill(.blue) .frame(width: geoProxy.size.width, height: 80) HStack(spacing: 100) { Button { DispatchQueue.main.async { scrollViewSavedValue.selectedTab = 1 } } label: { ZStack { Circle() .fill(.black) .frame(width: 60, height: 60) Text("View 1") .foregroundColor(.white) .font(.system(size: 12)) .fontWeight(.bold) } } Button { DispatchQueue.main.async { scrollViewSavedValue.selectedTab = 2 } } label: { ZStack { Circle() .fill(.black) .frame(width: 60, height: 60) Text("View 2") .foregroundColor(.white) .font(.system(size: 12)) .fontWeight(.bold) } } } } } .ignoresSafeArea() } } struct PresentationView: View { @StateObject private var scrollViewSavedValue = ScrollViewSavedValue() var body: some View { GeometryReader { geoProxy in switch scrollViewSavedValue.selectedTab { case 1: ScrollViewWithSavedPosition(scrollViewSavedValue: scrollViewSavedValue, geoProxy: geoProxy) default: Text("View 2") .foregroundColor(.black) } TabBarView(scrollViewSavedValue: scrollViewSavedValue, geoProxy: geoProxy) } } }
问题原因分析
- 硬编码滚动高度:
maxScrollableHeight是固定值,不同设备、屏幕尺寸下实际可滚动高度存在差异,接近底部时比例计算的误差被放大,导致scrollTo的anchor参数偏离真实位置,引发内容高度异常。 - 滚动恢复时机过早:
onAppear中直接调用scrollTo时,LazyVStack的内容可能还未完成渲染,ScrollView的布局尚未稳定,导致滚动位置计算错误。 - 偏移计算逻辑缺陷:使用全局坐标系的原点计算偏移,容易受父布局、安全区变化影响,且
isViewLoaded的异步设置存在竞态,导致偏移值计算不准确。
修复方案及代码
// 修复后的滚动视图 struct ScrollViewWithSavedPosition: View { @ObservedObject var scrollViewSavedValue: ScrollViewSavedValue @State private var contentHeight: CGFloat = 0 @State private var scrollViewHeight: CGFloat = 0 @State private var isViewReady = false var body: some View { ScrollViewReader { scrollProxy in ScrollView(showsIndicators: false) { LazyVStack { ForEach(0..<34) { poster in ZStack { Rectangle() .fill(.black) .frame(width: UIScreen.main.bounds.width * 0.4, height: UIScreen.main.bounds.height * 0.2) Text("\(poster)") .foregroundColor(.orange) } } } .background( GeometryReader { contentGeo in Color.clear .onAppear { contentHeight = contentGeo.size.height } } ) } .background( GeometryReader { scrollGeo in Color.clear .onAppear { scrollViewHeight = scrollGeo.size.height isViewReady = true } } ) .onAppear { guard isViewReady, contentHeight > scrollViewHeight else { return } // 转换保存的比例为实际像素偏移 let scrollableRange = contentHeight - scrollViewHeight let pixelOffset = scrollViewSavedValue.scrollOffsetValue * scrollableRange // 先滚动到底部元素,再调整anchor实现精确偏移 scrollProxy.scrollTo(33, anchor: .top) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { let adjustRatio = (pixelOffset - scrollableRange) / contentHeight scrollProxy.scrollTo(33, anchor: UnitPoint(x: 0, y: 1 + adjustRatio)) } } .onScrollChange { offset in // 基于真实可滚动范围计算比例 let scrollableRange = max(contentHeight - scrollViewHeight, 1) scrollViewSavedValue.scrollOffsetValue = -offset.y / scrollableRange } } .preferredColorScheme(.light) } } // 自定义滚动偏移监听修饰符(iOS 16+兼容,Xcode14支持) extension View { func onScrollChange(perform action: @escaping (CGPoint) -> Void) -> some View { self.background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).origin) } ) .coordinateSpace(name: "scrollView") .onPreferenceChange(ScrollOffsetKey.self, perform: action) } } struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGPoint = .zero static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {} } // 其他组件简化调整 class ScrollViewSavedValue: ObservableObject { @Published var scrollOffsetValue: CGFloat = 0 @Published var selectedTab: Int = 1 } struct TabBarView: View { @ObservedObject var scrollViewSavedValue: ScrollViewSavedValue var body: some View { VStack { Spacer() ZStack { Rectangle() .fill(.blue) .frame(height: 80) HStack(spacing: 100) { Button { scrollViewSavedValue.selectedTab = 1 } label: { ZStack { Circle() .fill(.black) .frame(width: 60, height: 60) Text("View 1") .foregroundColor(.white) .font(.system(size: 12, weight: .bold)) } } Button { scrollViewSavedValue.selectedTab = 2 } label: { ZStack { Circle() .fill(.black) .frame(width: 60, height: 60) Text("View 2") .foregroundColor(.white) .font(.system(size: 12, weight: .bold)) } } } } } .ignoresSafeArea() } } struct PresentationView: View { @StateObject private var scrollViewSavedValue = ScrollViewSavedValue() var body: some View { ZStack(alignment: .bottom) { switch scrollViewSavedValue.selectedTab { case 1: ScrollViewWithSavedPosition(scrollViewSavedValue: scrollViewSavedValue) default: Text("View 2") .foregroundColor(.black) .frame(maxWidth: .infinity, maxHeight: .infinity) } TabBarView(scrollViewSavedValue: scrollViewSavedValue) } } }
关键改进点
- 动态计算滚动范围:移除硬编码高度,通过GeometryReader实时获取内容和ScrollView容器的真实高度,确保比例计算准确。
- 优化滚动恢复时机:延迟执行
scrollTo操作,等待LazyVStack内容渲染完成、布局稳定后再调整位置。 - 更可靠的偏移监听:自定义
onScrollChange修饰符,基于ScrollView自身坐标系监听偏移,避免全局坐标系的干扰。 - 简化布局传递:移除GeometryProxy在组件间的传递,直接使用屏幕尺寸或内部GeometryReader获取布局信息,减少布局不确定性。
内容的提问来源于stack exchange,提问作者codingethics
相关产品推荐
相关产品推荐

