Well, honestly, I did it, because I needed it, and only then looked around and did not find anything on SO native in SwiftUI, so wanted to share. Thus this is just a self-answered question.
Initially I needed sticky stretchable sticky header for lazy content dependent only on ScrollView
.
Later (after I got my solution) I found this one on Medium, but I don't like it (and would not recommend at least as-is), because:
offset
(I don't like to use offset, because of its inconsistency with layout, etc.)So, actually all this text was just to fulfil SO question requirements - who knows me here knows that I don't like to type many text, it is better to type code 😀, in short - my approach is below in answer, maybe someone find it useful.
Initial code which SwiftUI gives us for free
ScrollView {
LazyVStack(spacing: 8, pinnedViews: [.sectionHeaders]) {
Section {
ForEach(0...100) {
Text("Item \($0)")
.frame(maxWidth: .infinity, minHeight: 60)
}
} header: {
Image("picture").resizable().scaledToFill()
.frame(height: 200)
}
}
}
Header is sticky by scrolling up, but not when down (dragged with content), and it is not stretchable.
Ok, we need to solve two problems:
ScrollView
on drag downA possible approach to solve this:
ScrollView
now manages content offsets privately (UIKit variants are out of topics here), so to pin to top using overlay ScrollView {
// ...
}
.overlay(
// >> any header
Image("picture").resizable().scaledToFill()
// << header end
.frame(height: imageHeight) // will calculate below
.clipped()
Use Section
default header (as placeholder) to calculate current distance from ScrollView
top
Section(...) {
// ...
} header: {
// here is only caculable part
GeometryReader {
// detect current position of header bottom edge
Color.clear.preference(key: ViewOffsetKey.self,
value: $0.frame(in: .named("area")).maxY)
}
.frame(height: headerHeight)
.onPreferenceChange(ViewOffsetKey.self) {
// prevent image negative height if header is not pinned
// for simplicity (can be optional, etc.)
imageHeight = $0 < 0 ? 0.001 : $0
}
}
That's actually it, everything else is just for demo part.
Tested with Xcode 13.4 / iOS 15.5