You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 11:22:07