I am currently trying to implement a scroll to hide header. How can I shrink the height of the header and fade it in as the user scrolls down. Then do the opposite when the user scrolls up(expand the header height to its initial value and fade it out).I have tried animating the opacity but it wasn't smooth. Here is the code for hiding and snapping the heading based on scroll direction.
struct ScrollToHideView: View { @State private var naturalScrollOffset: CGFloat = 0 @State private var lastNaturalOffset: CGFloat = 0 @State private var headerOffset: CGFloat = 0 @State private var isScrollingUp = false @State private var opacity = 1.0 @State private var tempH = 0.0 @State private var top = 0.0 var body: some View { GeometryReader { let safeArea = $0.safeAreaInsets let headerHeight = 60.0 + safeArea.top ScrollView(.vertical) { LazyVStack { ForEach(0..<10, id: \.self) { index in DummyView() } } .padding(16) } .overlay(content: { Text("\(naturalScrollOffset)") }) .safeAreaInset(edge: .top, spacing: 0, content: { HeaderView() .padding(.bottom, 16) .frame(height: headerHeight, alignment: .bottom) .background(.blue) .opacity(opacity) .offset(y: -headerOffset) }) .onScrollGeometryChange(for: CGFloat.self) { proxy in let maxHeight = proxy.contentSize.height - proxy.containerSize.height return max(min(proxy.contentOffset.y + headerHeight, maxHeight),0) } action: { oldValue, newValue in let isScrollingUp = oldValue < newValue headerOffset = min(max(newValue - lastNaturalOffset, 0), headerHeight) self.isScrollingUp = isScrollingUp // animating opacity withAnimation(.easeIn(duration: 2)) { if self.isScrollingUp { opacity = 0 } else { opacity = 1 } } naturalScrollOffset = newValue } .onScrollPhaseChange({ oldPhase, newPhase, context in if !newPhase.isScrolling && (headerOffset != 0 || headerOffset != headerHeight) { withAnimation(.snappy(duration: 0.25, extraBounce: 0)) { if headerOffset > (headerHeight * 0.5) && naturalScrollOffset > headerHeight { headerOffset = headerHeight } else { headerOffset = 0 } lastNaturalOffset = naturalScrollOffset - headerOffset } } }) .onChange(of: isScrollingUp) { oldValue, newValue in lastNaturalOffset = naturalScrollOffset - headerOffset } .ignoresSafeArea(.container, edges: .top) } }}extension ScrollToHideView { @ViewBuilder func HeaderView() -> some View { HStack(spacing: 20) { Image("user1") .resizable() .aspectRatio(contentMode: .fit) .frame(height: 25) Spacer(minLength: 0) Button("", systemImage: "airplayvideo") { } Button("", systemImage: "bell") { } Button("", systemImage: "magnifyingglass") { } } .font(.title2) .foregroundStyle(.primary) .padding(.horizontal, 16) } @ViewBuilder func DummyView() -> some View { VStack(alignment: .leading, spacing: 6) { RoundedRectangle(cornerRadius: 6) .frame(height: 220) HStack(spacing: 10) { Circle() .frame(width: 45, height: 45) VStack(alignment: .leading, spacing: 4) { Rectangle() .frame(height: 10) HStack { Rectangle() .frame(width: 100) Rectangle() .frame(width: 80) Rectangle() .frame(width: 80) } .frame(height: 10.0) } } } .foregroundStyle(.tertiary) }}