3

I've been fighting with GeometryReader and .onChange(of: geo.frame(in: .global).minY) for a long time trying to get this to work with no great success. Consider the following:

struct TestScreen: View {
    
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack {
                    Text("View A")
                        .frame(height: 50)
                        .frame(maxWidth: .infinity)
                        .background(.green)
                    Text("View B - Sticky")
                        .frame(height: 50)
                        .frame(maxWidth: .infinity)
                        .background(.blue)
                    ForEach(0..<15) { i in
                        Text("View \(i)")
                            .frame(height: 50)
                            .frame(maxWidth: .infinity)
                            .background(Color.red)
                    }
                }
                
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbarBackground(.clear, for: .navigationBar)
            .toolbar {
                ToolbarItem(placement: .principal) {
                    Text("Testing")
                        .foregroundColor(.purple)
                }
            }
        }
    }
}

The goal is to make View B stick to the top (just under the navigation bar) when you scroll the view upwards, and of course take its normal place in the scrollview when you scroll back down. I know you can do sticky headers with List and Sections but that doesn't suit my needs because note that View B (the sticky view) isn't necessarily the first item in the scrollview.

Also note that View B must stay on top of all other content in the VStack so that the other content scrolls beneath it.

1 Answer 1

8

Try using a LazyVStack with sections and pinned headers:

  • The first section contains View A, with no header.
  • The second section contains the ForEach as its main content and View B as its header.
ScrollView {
    LazyVStack(pinnedViews: .sectionHeaders) {
        Section {
            Text("View A")
                // ... modifiers as before
        }
        Section {
            ForEach(0..<15) { i in
                // ... content as before
            }
        } header: {
            Text("View B - Sticky")
                // ... modifiers as before
        }
    }
}

Animation

Sign up to request clarification or add additional context in comments.

3 Comments

Works like a charm.
how can we make it work better when the scroll view is ignoring top safe area with the use of modifier: .ignoresSafeArea(.container, edges: .top)? Instead of behaving View B as sticky at the top edge of the screen, I want it to behave sticky exactly below the navigation bar.
Try removing the modifier .ignoresSafeArea from the ScrollView. By default, the ScrollView will scroll through the safe area anyway, as you can just detect from the gif in the answer. If the background to the navigation bar would be hidden then of course you would see it better. Otherwise, if it's not as simple as that, I would suggest posting a new question, with an MRE to demonstrate the issue.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.