LazyVStack with pinned section headers overlaps items when using a ScrollViewProxy's scrollTo method

Originator:tyler.hedrick
Number:rdar://FB13447486 Date Originated:12/5/23
Status:Open Resolved:Open
Product:SwiftUI Product Version:iOS 17.0
Classification:Incorrect/Unexpected Behavior Reproducible:Yes
 
I have a list of items in a LazyVStack inside of a ScrollView where I would like to be able to scroll to a specific item programmatically and have it anchored at the top of the screen. This screen also has a pinned section header as our designer wants to have a search bar always available to the user as they scroll through the list of items. The issue is that when I use ScrollViewProxy.scrollTo(ItemID, anchor: .top) it will scroll the item to the very top of the screen without accounting for the pinned header. This has the result of the item being completely hidden behind the header, instead of being pinned just under where the header ends. To reproduce: 
* Copy paste the provided code below into a new SwiftUI iOS app project 
* Run the project to a simulator, device, or use the Xcode Preview (I am using an iPhone 15 Pro simulator) 
* Tap the button at the top that says "Scroll to item 60" 

Expected result:
* The scroll view should scroll and stop with "Item 60" clearly visible at the top 

Result observed: 
* Item 60 is offscreen, and Item 64 is actually at the top instead 

I think there are a couple of ways to fix this issue: 
* The behavior could be updated to take into account the section header size by default (preferred) 
* ScrollViewProxy could accept an `offset` parameter to allow the developer to provide an offset value (similar to UIScrollView which has this option) Xcode version: 15.0.1 iOS Version: 17.0.1

```
struct ContentView: View {
  @State var scrollItemID = 0

  var body: some View {
    ScrollViewReader { proxy in
      ScrollView {
        LazyVStack(
          alignment: .leading,
          spacing: 0,
          pinnedViews: [.sectionHeaders])
        {
          Section {
            Button("Scroll to item 60") {
              scrollItemID = 60
            }
            .padding()

            ForEach(1...1000, id: \.self) { count in
              Row(text: "Item \(count)")
                .id(count)
              Divider()
            }
            .onChange(of: scrollItemID) { oldValue, newValue in
              if newValue == 0 { return }
              withAnimation {
                proxy.scrollTo(newValue, anchor: .top)
              }
              scrollItemID = 0
            }
          } header: {
            StickyHeader()
          }
        }
      }
      .ignoresSafeArea()
    }
  }
}

struct StickyHeader: View {
  var body: some View {
    Text("This is a sticky header. When you tap \"Scroll to item 60\" below, it should scroll to have Item 60 at the top. However, this sticky header is overlapping that row, and it looks like we scroll to a different row entirely.")
      .frame(maxWidth: .infinity, alignment: .leading)
      .padding()
      .padding(.top, 50)
      .background(Color.green)
  }
}

struct Row: View {
  let text: String

  var body: some View {
    Text(text)
      .frame(maxWidth: .infinity, alignment: .leading)
      .padding()
  }
}

#Preview {
  ContentView()
}
```

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!