I’m working on a SwiftUI screen where I need to hide a header when the user scrolls down and show it again when the user scrolls up. I’m currently using a ScrollView combined with GeometryReader to detect scroll offset changes and update state variables like isScrolling or isScrollingDown. The issue is that the behavior is inconsistent. When I scroll down, the header hides correctly, but when I scroll back up, the header often doesn’t appear again even though the offset is changing. Sometimes the header comes back with a delay, and other times it never appears at all. Along with this, I’m also seeing noticeable UI lag whenever I try to calculate content height or read multiple geometry values inside the ScrollView. It looks like the frequent state updates inside the scroll offset tracking are causing layout recalculations and frame drops. I’ve tried placing the header in different positions (inside a ZStack aligned to the top, inside the VStack above the ScrollView, and with transitions like .push(from: .top)), but the result is still the same: smooth scrolling breaks, and the header doesn’t reliably animate back when scrolling upward. What I’m looking for is a minimal and efficient approach to detect scroll direction and trigger the header hide/show animation without causing performance issues or recomputing expensive layout values. Any guidance or a simplified pattern that works well for dynamic headers in SwiftUI would be very helpful.
if isScrolling { headerStackView() //Includes Navigation Bar .transition( .asymmetric( insertion: .push(from: .top), removal: .push(from: .bottom) ) ) } GeometryReader { outer in let outerHeight = outer.size.height
ScrollView(.vertical) {
VStack {
content() // Heavy view + contains its own ScrollView
}
.background {
GeometryReader { proxy in
let contentHeight = proxy.size.height
let minY = max(
min(0, proxy.frame(in: .named("ScrollView")).minY),
outerHeight - contentHeight
)
if #available(iOS 17.0, *) {
Color.clear
.onChange(of: minY) { oldVal, newVal in
// Scroll direction detection
if (isScrolling && newVal < oldVal) ||
(!isScrolling && newVal > oldVal) {
isScrolling = newVal > oldVal
}
}
}
}
}
}
.coordinateSpace(name: "ScrollView")
} .padding(.top, 1)