swiftuiswiftui-scrollview

SwiftUI scrollview sticky header without using List


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.


Solution

  • Try using a LazyVStack with sections and pinned headers:

    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