I want to create a custom header view that is inside a vstack and on scroll up diminish the height of that custom header. I followed this blog: https://www.bigmountainstudio.com/community/public/posts/13099-swiftui-geometryreader-sticky-header-when-scrolling-part-5
The difference I made from the blog was substitute Image("Utah") with a customView
ScrollView {
ZStack(alignment: .top) {
// Bottom Layer
VStack(spacing: 20) {
ListContentView()
}
.padding(.horizontal, 20)
.padding(.top, 128)
// Top Layer (Header)
GeometryReader { gr in
VStack(alignment: .leading, spacing: 0) {
Spacer().frame(height: 32)
navigationHeader
.frame(height:
self.calculateHeight(minHeight: 44,
maxHeight: 128,
yOffset: gr.frame(in: .global).minY)
)
// sticky it to the top
.offset(y: gr.frame(in: .global).origin.y < 0 // Is it going up?
? abs(gr.frame(in: .global).origin.y) // Push it down!
: -gr.frame(in: .global).origin.y) // Push it up!
Spacer()
}
.border(.red)
}
}
}
.navigationBarHidden(true)
.background(.black)
}
func calculateHeight(minHeight: CGFloat, maxHeight: CGFloat, yOffset: CGFloat) -> CGFloat {
// If scrolling up, yOffset will be a negative number
if maxHeight + yOffset < minHeight {
// SCROLLING UP
// Never go smaller than our minimum height
return minHeight
}
// SCROLLING DOWN
return maxHeight
}
private var navigationHeader: some View {
Group {
Image("Back")
Text("Title")
.h2()
Spacer.withHeight(Spacing.C4)
Text("some subtitle text some subtitle text some subtitle text some subtitle text some subtitle text")
.subheader()
.fixedSize(horizontal: false, vertical: true)
}
}
Problem is setting the frame of the VStack inside the scrollView. If I comment the .frame modifier the VStack uses the whole space of the scrollView and tries to fit it all on top:
If I try to use frame to set the offset calculate by scrolling, the custom header view tries to use all the space and ignores the frame as seen here:
Any idea how to achieve this behaviour shown here?
Sorry for the long post and thank you so much in advance for any ideias or other approaches to achieve this animation on scroll up. I have also tried setting .navigationBar() but that only allows text. Also tried .navigationBarItems(leading: customView) but that only goes till half of the screen on top.
Here comes an approach. To show/hide the description I changed navigationHeader
to a func passing in whether to show or not, based on current header height. The orange background is for test purpose only.
struct ContentView: View {
var body: some View {
ScrollView {
ZStack(alignment: .top) {
// Bottom Layer
VStack(spacing: 20) {
// dummy list
ForEach(0..<30) { item in
Text("List Item \(item)")
.frame(maxWidth: .infinity)
.padding()
.background(.gray)
.cornerRadius(10)
}
}
.padding(.horizontal, 20)
.padding(.top, 128)
// Top Layer (Header)
GeometryReader { gr in
VStack {
// save height as it is used at two places
let h = self.calculateHeight(minHeight: 32,
maxHeight: 80,
yOffset: gr.frame(in: .global).minY)
Color.clear.frame(height: 32)
navigationHeader(showDescr: h > 50)
.padding().foregroundColor(.white)
.background(.orange) // for demo purpose only
.frame(height: h)
// sticky it to the top
.offset(y: gr.frame(in: .global).origin.y < 0 // Is it going up?
? abs(gr.frame(in: .global).origin.y) // Push it down!
: -gr.frame(in: .global).origin.y) // Push it up!
Spacer()
}
//.border(.red)
}
}
}
.navigationBarHidden(true)
.background(.black)
}
func calculateHeight(minHeight: CGFloat, maxHeight: CGFloat, yOffset: CGFloat) -> CGFloat {
// If scrolling up, yOffset will be a negative number
if maxHeight + yOffset < minHeight {
// SCROLLING UP
// Never go smaller than our minimum height
return minHeight
}
// SCROLLING DOWN
return maxHeight + yOffset
}
private func navigationHeader(showDescr: Bool) -> some View {
Group {
if showDescr { // show/hide description
VStack(alignment: .leading) { // Vstack instead of Group
Image(systemName: "arrow.left")
Text("Title")
.font(.largeTitle)
Text("some subtitle text some subtitle text some subtitle text some subtitle text some subtitle text")
.font(.subheadline)
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxHeight: .infinity)
} else {
HStack {
Image(systemName: "arrow.left")
Spacer()
Text("Title")
.font(.headline)
Spacer()
}
}
}
}
}