swiftuiscrollviewreader

How to Ensure ScrollViewReader.scrollTo Considers Section Header Height in SwiftUI?


I'm working on a SwiftUI view that includes a ScrollView with a LazyVStack and section headers. My goal is to programmatically scroll to a specific item in the list using ScrollViewReader.scrollTo, but I've encountered an issue where the content scrolls under the section header, making the top part of the targeted item obscured by the header.

Here's the simplified code snippet that demonstrates the structure of my view:

struct ContentView: View {
var body: some View {
    NavigationView {
        ScrollViewReader { proxy in
            ScrollView(showsIndicators: false) {
                Color.cyan
                    .frame(width: 360, height: 200)
                LazyVStack(spacing: 8, pinnedViews: [.sectionHeaders]) {
                    Section {
                        ForEach(0..<100) { i in
                            ZStack {
                                Color.green
                                Text(i.description)
                                    .font(.subheadline)
                            }
                            .id(i)
                            .frame(width: 50, height: 50)
                        }
                    } header: {
                        Color.red
                            .frame(width: 360, height: 60)
                            .opacity(0.4)
                    }
                }
                .navigationTitle("Info")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: {
                            DispatchQueue.main.async {
                                withAnimation(Animation.easeInOut(duration: 0.3)) {
                                    proxy.scrollTo(77, anchor: .top)
                                }
                            }
                        }, label: {
                            Image(systemName: "house")
                        })
                    }
                }
            }
        }
    }
}

}

When I tap the button in the navigation bar to scroll to item 77, the item ends up partially hidden under the section header. I understand that ScrollViewReader.scrollTo does not account for the height of the pinned section header when calculating the scroll position.

Has anyone faced a similar issue and found a workaround to ensure that the scroll target is fully visible, considering the section header's height? Any suggestions or insights would be greatly appreciated.


Solution

  • You would need to know 3 things:

    After that, you can compute a custom UnitPoint as the anchor: argument.

    // assuming you already wrapped the scroll view in a GeometryReader { geo in ... }
    let scrollViewHeight = geo.size.height
    let sectionHeaderHeight: CGFloat = 60
    let contentHeight: CGFloat = 50
    let unitY = (contentHeight * sectionHeaderHeight) / (scrollViewHeight - contentHeight) / contentHeight
    proxy.scrollTo(77, anchor: .init(x: 0.5, y: unitY))
    

    If the section header height is dynamic, you can still read it from the geometry proxy using bounds(of:). Here is an example for reading the section header height:

    } header: {
        SomeViewWithDynamicHeight()
            .coordinateSpace(name: "Section")
    }
    
    let sectionHeaderHeight = geo.bounds(of: .named("Section"))?.height ?? 0