ARTICLE AD BOX
I would suggest simply adding padding to the top of the VStack when the view is pulled down. This will allow the date and time to be seen behind the VStack. Some notes:
.onScrollGeometryChange can be used to detect when the view is pulled down. You might want to inspect the scroll phase too, so that the header is only shown when the user interacts with the scroll view from rest, not when scrolling long content back into view. To do this, .onScrollPhaseChange can be used. There is a version of .onScrollPhaseChange that provides ScrollPhaseChangeContext to the closure. This makes it possible to examine the scroll offset when the phase changes too. However, I found that it only reports the offset at the moment of phase change, not on a continuous basis. So .onScrollGeometryChange is probably needed too. If you want to mask the date and time when the view is not pulled down then the background probably needs to be applied to the VStack. If the content is short (as in your example), the background can be extended to the bottom of the screen simply by adding a large amount of negative bottom padding. Alternatively, the height for the background could be computed if the geometry of the screen is known, but negative padding is perhaps simpler (and fit for purpose).Here is an elaborated example to show it all working:
struct ContentView: View { let headerHeight: CGFloat = 50 let scrollThreshold: CGFloat = 10 @State private var isInteractingFromRest = false @State private var isHeaderShowing = false private var dateAndTime: some View { // ... } private var storyContent: some View { // ... } var body: some View { NavigationStack { ZStack(alignment: .top) { dateAndTime ScrollView { VStack { storyContent } .frame(maxWidth: .infinity, alignment: .leading) .background { Color.blue .padding(.bottom, -2000) } .padding(.top, isHeaderShowing ? headerHeight : 0) } .onScrollPhaseChange { _, newPhase, context in if newPhase == .interacting { let geo = context.geometry let scrollOffset = geo.contentOffset.y + geo.contentInsets.top isInteractingFromRest = abs(scrollOffset) < 1 } else { isInteractingFromRest = false } } .onScrollGeometryChange(for: Bool.self) { geo in let result: Bool if isInteractingFromRest { let scrollOffset = geo.contentOffset.y + geo.contentInsets.top result = scrollOffset < (isHeaderShowing ? scrollThreshold : -scrollThreshold) } else { result = isHeaderShowing } return result } action: { _, newVal in if newVal != isHeaderShowing { withAnimation { isHeaderShowing = newVal } } } .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { Button("Edit") {} Button("Delete", systemImage: "trash") {} } } } .background(.red.opacity(0.5)) } } }
31k3 gold badges23 silver badges67 bronze badges
2 Comments
The logic for switching the visibility might have been a bit flawed, I've made a correction to the answer. Re. not working for you, did you try copy/pasting the code in the answer? The only bits missing are the implementations of dateAndTime and storyContent, which can be kept simple. Otherwise, were you were planning to update the question with the changes you tried?
2026-02-25T21:43:15.397Z+00:00
Explore related questions
See similar questions with these tags.

