I'm trying to create sticky header with manual scroll to functionality (eg. categories bar allowing user to move to chosen section). I have issues with one part of that functionallity. While ScrollViewReader and proxy.scrollTo works as intended; it keeps scrolling under sticky header instead of below it. Image one shows how it works, image two shows what is expected. As you can see swiftUI basically ignores existence of header, treating it as separate Z layer.
I would love solution to work on iOS 15.
Code to reproduce example:
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)
}
}
How does it work now (when tapping scroll to id of section 3)

How does I would like it to work (scrolls so section title is visible, and not covered by sticky header)

The documentation for
.scrollToexplains how theanchorworks:So when the anchor is
.topas you have it, the top of the target view will be aligned with the top of theScrollView. This is exactly what you are seeing -> working as designed.What you want is for the target section to be below the top of the
ScrollViewby the height of the header. One way to achieve this is to change thescrollTotarget to something that is at the end of the preceding section.To see it working, try the following changes to your example:
1. Add an additional id to
SectionData, to use as thescrollTotarget2. Add a placeholder to the bottom of the background of each
SectionViewWhile we're at it, we can fix the
ForEachto prevent errors in the console about duplicate ids.If you don't know the height that needs to be reserved then you could use a hidden copy of
Headerinstead:3. Change the scrollTo target to refer to the placeholder at the bottom of the preceding section
You might like to add some animation too:
4. Change
NestedSectionsViewto useVStackinstead ofLazyVStackIf a
LazyVStackis used then the placeholders that were added to the background in step 2 do not exist when aSectionViewis out-of-view, so they can't be used for scrolling to. Changing to aVStackresolves this:It now scrolls to the desired position: