macOS下SwiftUI中含不同高度项的LazyVStack与ScrollView兼容问题
问题解决:macOS 13 中 LazyVStack 嵌套 ScrollView 的滚动跳跃问题
问题回顾
在 macOS 13 系统中,当LazyVStack包含不同高度的项目时,外层ScrollView会出现滚动位置跳跃的问题,缓慢滚动时尤为明显,生产环境中甚至可能无法向上滚动。相同代码在 iOS 上仅会出现滚动指示器高度变化,滚动过程保持平滑。由于性能需求及粘性头部功能需要,必须使用LazyVStack而非常规VStack。
可行解决方案
针对这个 macOS 特有的布局计算问题,可通过以下几种方式修复:
1. 强制 Item 高度稳定
给每个Item视图设置最小高度,配合固定尺寸属性,避免 LazyVStack 在懒加载时动态调整高度导致滚动偏移:
struct Item: View { let value: Int let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." var body: some View { Group { if value < 20 { Text("\(value) A long text: \(lorem)") } else { Text("\(value) A short text") } } .frame(minHeight: 80) // 根据实际内容调整最小高度 .fixedSize(horizontal: false, vertical: true) } }
2. 配置滚动目标布局(macOS 13+ 支持)
将LazyVStack标记为滚动目标布局,配合ScrollView的滚动行为配置,让系统更精准地计算滚动位置:
struct ContentView: View { var body: some View { ScrollView { LazyVStack { ForEach(1...40, id: \.self) { value in Item(value: value) .id(value) .padding() } } .padding() .scrollTargetLayout() // 添加滚动目标布局标记 } .frame(width: 300, height: 300) .scrollTargetBehavior(.viewAligned) // 设置视图对齐的滚动行为 } }
3. 预缓存 Item 高度
通过偏好键提前计算并缓存每个 Item 的高度,让LazyVStack基于预计算值布局,避免动态高度引发的滚动波动:
// 定义用于传递高度的偏好键 struct ItemHeightPreferenceKey: PreferenceKey { static var defaultValue: [Int: CGFloat] = [:] static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) { value.merge(nextValue()) { $1 } } } struct Item: View { let value: Int let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." var body: some View { Group { if value < 20 { Text("\(value) A long text: \(lorem)") } else { Text("\(value) A short text") } } .background( GeometryReader { proxy in Color.clear .preference(key: ItemHeightPreferenceKey.self, value: [value: proxy.size.height]) } ) } } struct ContentView: View { @State private var itemHeights: [Int: CGFloat] = [:] var body: some View { ScrollView { LazyVStack { ForEach(1...40, id: \.self) { value in Item(value: value) .id(value) .padding() .frame(height: itemHeights[value]) // 使用缓存的高度 } } .padding() .onPreferenceChange(ItemHeightPreferenceKey.self) { itemHeights = $0 } } .frame(width: 300, height: 300) } }
原理说明
macOS 上的LazyVStack在处理动态高度视图时,布局计算逻辑与 iOS 存在差异:iOS 会提前预估视图高度并维持滚动偏移稳定,而 macOS 仅在视图懒加载时计算实际高度,导致滚动容器的内容尺寸突然变化,进而引发滚动位置跳跃。上述解决方案通过强制高度稳定、优化滚动布局逻辑或预缓存高度,避免了动态高度带来的布局波动。
内容的提问来源于stack exchange,提问作者viedev
相关产品推荐
相关产品推荐

