SwiftUI切换设备方向时如何从0重算ScrollView元素yOffset
问题根因
原实现存在三个核心问题导致方向切换后选中状态不同步:
- 缓存的子项偏移值是基于ScrollView视口坐标系的动态值,不是内容层的固定相对位置,滚动、尺寸变化、方向切换时该值会实时变动,首次加载后一次性缓存的数值会直接失效
- 子项偏移采集逻辑加了
numberPreferenceKeys.count < 6的采集上限,还附带了仅对number=1做偏移补正的硬编码补丁,方向切换清空数组后重新采集的数值完全不准 - 滚动偏移PreferenceKey的reduce逻辑用了累加计算,会导致滚动偏移值统计错误
修复方案
不要缓存静态偏移值,每次布局变化时实时计算每个子项相对于ScrollView内容起点的固定位置,不需要监听方向切换通知,也不需要手动重置缓存:
- 拆分两层独立坐标空间:外层ScrollView视口坐标空间用来计算当前滚动距离,内层内容容器坐标空间用来获取子项相对于内容起点的固定偏移,该偏移和当前滚动位置、设备方向无关
- 用字典存储子项偏移,避免数组重复添加、顺序错乱问题,布局刷新时自动更新值
- 修正滚动偏移PreferenceKey的reduce逻辑,直接取内容容器的实时偏移,不做累加
- 优化选中项匹配规则:以屏幕中线为阈值,匹配起点在阈值左侧的最后一个子项,避免跳选问题
修复后可直接运行的代码
import SwiftUI // 存储每个区块的相对位置 struct ItemOffsetPreferenceKey: PreferenceKey { static var defaultValue: [Int: CGFloat] = [:] static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) { value.merge(nextValue(), uniquingKeysWith: { $1 }) } } // 实时获取滚动偏移值 struct ScrollOffsetPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct ContentView: View { @State private var itemOffsets: [Int: CGFloat] = [:] @State private var selectedNumber = 0 @State private var rectangleWidths: [CGFloat] = (0..<5).map { _ in CGFloat.random(in: 500..<2000) } let colors: [Color] = [.blue, .red, .green, .gray, .purple] var body: some View { VStack { ScrollViewReader { reader in ScrollView(.horizontal) { // 内容容器单独设置命名坐标空间,子项在该空间下的minX为固定相对位置 HStack(spacing: 0) { ForEach(0..<5) { number in Rectangle() .fill(colors[number]) .frame(width: rectangleWidths[number], height: 200) .id("\(number)") .background( GeometryReader { proxy in Color.clear .preference( key: ItemOffsetPreferenceKey.self, value: [number: proxy.frame(in: .named("scrollContent")).minX] ) } ) } } .coordinateSpace(name: "scrollContent") .background( GeometryReader { contentProxy in Color.clear .preference( key: ScrollOffsetPreferenceKey.self, value: -contentProxy.frame(in: .named("scrollView")).origin.x ) } ) } .coordinateSpace(name: "scrollView") .onPreferenceChange(ItemOffsetPreferenceKey.self) { newOffsets in itemOffsets = newOffsets } .onPreferenceChange(ScrollOffsetPreferenceKey.self) { scrollOffset in let threshold = scrollOffset + UIScreen.main.bounds.width / 2 selectedNumber = itemOffsets .filter { $0.value <= threshold } .max(by: { $0.value < $1.value })? .key ?? 0 } // 底部导航栏 HStack(spacing: 20) { ForEach(0..<5) { number in ZStack { if number == selectedNumber { Rectangle() .fill(.black) .frame(width: 30, height: 30) } Rectangle() .fill(colors[number]) .frame(width: 25, height: 25) .onTapGesture { withAnimation { reader.scrollTo("\(number)", anchor: .leading) } } } } } .padding(.vertical) } } } }
内容的提问来源于stack exchange,提问作者Hugo de Groot
相关产品推荐
相关产品推荐

