3

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:

enter image description here

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:

enter image description here

Any idea how to achieve this behaviour shown here?

enter image description 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.

1 Answer 1

3

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.

enter image description here

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()
                }
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

6 Comments

After some tests finally found out what was the problem: you added a VStack(alignment: .leading) inside the navigationHeader. That fixed the problem on the second image of my question. Plus you added the change of frame, thank you so much. I'm going to try to add a transition animation between the navigation bar views, but you saved my life already. What I didn't figured it out, is that I don't see the orange frame growing down you when one scrolls down. Also it seems the stickyness is not working when scrolling a bit up (before changing to the inline navigationbar view).
Yes, I commented the VStack in the code but forgot to write in the answer :) I did try a quick animation/transition test but didn't get it to work fast enough so I skipped it. Good look with it, might be interesting how you solve it.
for the stickiness ... it does work with me (Xcode 14b, iOS 16), my posted code should reproduce that ...? I did change the min and max heights, maybe that?
no, I did a copy mistake ... there is a .frame(maxHeight: .infinity) missing in header view, I'll correct it in the answer.
Yup that .frame(maxHeight: .infinity) fixes the growing down. Also I fixed that stickyness problem. The calculateHeight the return maxHeight + yOffset should be replaced with just return maxHeight. I'm going to correct the code in the question.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.