SwiftUI动态内容下水平与垂直ScrollView同步问题求助
SwiftUI 双滚动区域同步失效问题(章节条目数动态变化时)
我正在构建一个包含两个同步滚动区域的SwiftUI视图:
- 水平ScrollView:展示章节列表
- 垂直ScrollView:展示对应章节的内容
问题说明:
当每个章节的条目数量统一时,实现可以正常工作,但当章节包含不同数量的条目时,同步机制失效,垂直ScrollView经常滚动到错误章节。
示例代码:
struct ContentView: View { // 示例数据 private let sections = (1...10).map { sectionIndex in SectionData( name: "Section \(sectionIndex)", items: (1...(Int.random(in: 80...150))).map { "Item \($0)" } ) } @State private var selectedSection: String? = nil @State private var currentVisibleSection: String? = nil var body: some View { VStack(spacing: 0) { // 水平章节选择器 ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(sections) { section in Button(action: { selectedSection = section.name }) { Text(section.name) .font(.headline) .padding(.horizontal, 10) .padding(.vertical, 5) .background( RoundedRectangle(cornerRadius: 10) .fill(currentVisibleSection == section.name ? Color.blue : Color.gray.opacity(0.2)) ) .foregroundColor(currentVisibleSection == section.name ? .white : .primary) } } } .padding() } .background(Color(UIColor.systemGroupedBackground)) // 垂直滚动内容区 ScrollViewReader { proxy in ScrollView(.vertical, showsIndicators: false) { LazyVStack(spacing: 20) { ForEach(sections) { section in VStack(alignment: .leading, spacing: 10) { // 章节头部 SectionHeader(name: section.name) .id(section.name) // 每个章节的唯一标识 // 章节内容 LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 10) { ForEach(section.items, id: \.self) { item in Text(item) .frame(height: 100) .frame(maxWidth: .infinity) .background(Color.blue.opacity(0.2)) .cornerRadius(8) } } } .background( GeometryReader { geo in Color.clear.preference( key: VisibleSectionPreferenceKey.self, value: [section.name: calculateVisibleHeight(geo)] ) } ) } } .onPreferenceChange(VisibleSectionPreferenceKey.self) { visibleSections in updateLargestVisibleSection(visibleSections) } .onChange(of: selectedSection) { sectionName in guard let sectionName else { return } withAnimation { proxy.scrollTo(sectionName, anchor: .top) } } } } } } // 更新当前可见面积最大的章节 private func updateLargestVisibleSection(_ visibleSections: [String: CGFloat]) { if let largestVisibleSection = visibleSections.max(by: { $0.value < $1.value })?.key { currentVisibleSection = largestVisibleSection } } // 计算章节的可见高度 private func calculateVisibleHeight(_ geometry: GeometryProxy) -> CGFloat { let frame = geometry.frame(in: .global) let screenHeight = UIScreen.main.bounds.height return max(0, min(frame.maxY, screenHeight) - max(frame.minY, 0)) } } // 跟踪可见章节的PreferenceKey private struct VisibleSectionPreferenceKey: PreferenceKey { static var defaultValue: [String: CGFloat] = [:] static func reduce(value: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) { value.merge(nextValue(), uniquingKeysWith: max) } } // 辅助视图与模型 struct SectionHeader: View { let name: String var body: some View { Text(name) .font(.headline) .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray.opacity(0.2)) } } struct SectionData: Identifiable { var id: String { name } let name: String let items: [String] }
现有方案的优缺点:
- LazyVStack:性能表现良好,但在章节条目数动态变化时同步失效
- VStack:可解决同步问题,但在大数据集下因预加载所有内容导致性能不佳
- 此外,在VStack中与LazyVGrid这类懒加载子视图交互时会出现滚动跳变,影响用户体验
- onPreferenceChange:使用自定义PreferenceKey跟踪可见章节,但该方法在懒加载章节和动态条目数场景下不可靠
内容的提问来源于stack exchange,提问作者Hussam
相关产品推荐
相关产品推荐

