Quick SwiftUI tip for today. If you don't like that annoying white space a user sees when they scroll past the top of your ScrollView, a Sticky Header works perfectly!

Place this StickyHeader component at the top of a ScrollView and any content you put inside will stretch in size to fill that gap when a user scrolls to the top. See the video below for example!

struct StickyHeader<Content: View>: View {

    var minHeight: CGFloat
    var content: Content
    
    init(minHeight: CGFloat = 200, @ViewBuilder content: () -> Content) {
        self.minHeight = minHeight
        self.content = content()
    }
    
    var body: some View {
        GeometryReader { geo in
            if(geo.frame(in: .global).minY <= 0) {
                content
                    .frame(width: geo.size.width, height: geo.size.height, alignment: .center)
            } else {
                content
                    .offset(y: -geo.frame(in: .global).minY)
                    .frame(width: geo.size.width, height: geo.size.height + geo.frame(in: .global).minY)
            }
        }.frame(minHeight: minHeight)
    }
}

How To Use

Here are some quick examples of how you can use it in your next project.

Sticky Header Image

var body: some View {
    ScrollView(.vertical, showsIndicators: false) {
        StickyHeader {
            ZStack {
                Color(red: 35/255, green: 45/255, blue: 50/255)
                VStack {
                    Text("Joshua Tree")
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                    Text("California")
                        .font(.title2)
                        .fontWeight(.semibold)
                        .foregroundColor(.white)
                }
            }
        }
        
        // Scroll View Content Here
        // ...
    }
}

Sticky Header Title / Description

var body: some View {
    ScrollView(.vertical, showsIndicators: false) {
        StickyHeader {
            StickyHeader {
                Image("cover")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            }
        }
        
        // Scroll View Content Here
        // ...
    }
}