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

SwiftUI:ScrollViewReader scrollTo在LazyVStack固定Header下滚动异常

问题描述
  • 开发带手动滚动功能的粘性头部时,使用ScrollViewReader和proxy.scrollTo会导致目标元素被粘性Header遮挡,SwiftUI未将Header的高度纳入滚动计算。
  • 当前效果:点击滚动到Section 3时,绿色标题栏被黄色粘性Header完全遮挡。
  • 预期效果:Section标题完整显示在粘性Header下方,无遮挡。
复现代码
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            ScrollViewReader(content: { proxy in
                ScrollView {
                    LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                        Image(systemName: "globe")
                            .imageScale(.large)
                            .foregroundStyle(.tint)
                            .onTapGesture {
                                proxy.scrollTo(sections[2].hashValue, anchor: .top)
                            }
                                                
                        Section {
                            NestedSectionsView()
                                .frame(maxWidth: .infinity)
                            
                        } header: {
                            Header().opacity(0.5)
                        }
                    }
                }
            })
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

let sections = [
    SectionData(header: "First Section", items: Array(repeating: "Item 1", count: 5)),
    SectionData(header: "Second Section", items: Array(repeating: "Item 2", count: 5)),
    SectionData(header: "Third Section", items: Array(repeating: "Item 3", count: 10))
]


struct NestedSectionsView: View {
    var body: some View {
        LazyVStack(spacing: 10) {
            ForEach(sections, id: \.hashValue) { section in
                SectionView(section: section)
            }
        }
        .padding()
        .navigationBarTitle("Nested Sections")
    }
}

struct SectionData: Identifiable, Hashable {
    let id = UUID()
    let header: String
    let items: [String]
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

struct SectionView: View {
    let section: SectionData
    
    var body: some View {
        VStack(alignment: .leading, spacing: 3) {
            Text(section.header)
                .font(.headline)
                .frame(height: 50)
                .frame(maxWidth: .infinity)
                .background(Color.green)
                .id(section.hashValue)
            
            ForEach(section.items, id: \.self) { item in
                Text(item)
                    .frame(height: 140)
                    .frame(maxWidth: .infinity)
                    .background(Color.red)
            }
        }
    }
}

struct Header: View {
    var body: some View {
        VStack {
            Image(systemName: "photo")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: UIScreen.main.bounds.width, height: 100)
                .padding()
            
            Text("Example Text")
                .font(.title)
                .foregroundColor(.black)
                .frame(width: UIScreen.main.bounds.width, height: 100, alignment: .center)
        }.background(Color.yellow)
    }
}
当前效果

当前效果:Section 3标题被黄色粘性Header遮挡

预期效果

预期效果:Section 3标题完整显示在黄色Header下方

iOS 15适配解决方案

核心思路:通过动态获取粘性Header的高度,在目标Section标题上方添加一个高度等于Header的隐形占位视图,将滚动目标绑定到这个占位视图上,使滚动后标题刚好显示在Header下方。

步骤1:获取Header高度

在ContentView中添加状态变量存储Header高度,通过GeometryReader动态获取:

struct ContentView: View {
    @State private var headerHeight: CGFloat = 0 // 存储Header高度
    
    var body: some View {
        VStack {
            ScrollViewReader(content: { proxy in
                ScrollView {
                    LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
                        Image(systemName: "globe")
                            .imageScale(.large)
                            .foregroundStyle(.tint)
                            .onTapGesture {
                                proxy.scrollTo(sections[2].hashValue, anchor: .top)
                            }
                                                
                        Section {
                            NestedSectionsView(headerHeight: headerHeight)
                                .frame(maxWidth: .infinity)
                            
                        } header: {
                            Header()
                                .opacity(0.5)
                                .background(GeometryReader { geo in
                                    Color.clear.onAppear {
                                        headerHeight = geo.size.height
                                    }
                                })
                        }
                    }
                }
            })
        }
        .padding()
    }
}

步骤2:传递Header高度到子视图

更新NestedSectionsView接收Header高度,并传递给SectionView:

struct NestedSectionsView: View {
    let headerHeight: CGFloat // 接收Header高度
    
    var body: some View {
        LazyVStack(spacing: 10) {
            ForEach(sections, id: \.hashValue) { section in
                SectionView(section: section, headerHeight: headerHeight)
            }
        }
        .padding()
        .navigationBarTitle("Nested Sections")
    }
}

步骤3:添加隐形占位视图

修改SectionView,在标题上方添加高度等于Header的隐形视图,并将滚动目标ID绑定到这个视图:

struct SectionView: View {
    let section: SectionData
    let headerHeight: CGFloat // 接收Header高度
    
    var body: some View {
        VStack(alignment: .leading, spacing: 3) {
            // 隐形占位视图,高度等于Header,绑定滚动ID
            Color.clear
                .frame(height: headerHeight)
                .id(section.hashValue)
            
            Text(section.header)
                .font(.headline)
                .frame(height: 50)
                .frame(maxWidth: .infinity)
                .background(Color.green)
            
            ForEach(section.items, id: \.self) { item in
                Text(item)
                    .frame(height: 140)
                    .frame(maxWidth: .infinity)
                    .background(Color.red)
            }
        }
    }
}

原理说明

  • 动态获取Header高度确保适配不同设备和布局变化
  • 隐形占位视图不会影响正常滚动时的UI展示
  • 将滚动目标绑定到占位视图顶部,滚动后Section标题自然落在Header下方,完全避免遮挡

内容的提问来源于stack exchange,提问作者Kamajabu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 18:28:10