Forked from JohnSundell/ContentViewWithCollapsableHeader.swift
Created
February 4, 2023 03:41
-
-
Save xta/498899aa2191b6325a00ad684d7cbaf0 to your computer and use it in GitHub Desktop.
A content view which renders a collapsable header that adapts to the current scroll position. Based on OffsetObservingScrollView from https://swiftbysundell.com/articles/observing-swiftui-scrollview-content-offset.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// View that renders scrollable content beneath a header that | |
/// automatically collapses when the user scrolls down. | |
struct ContentView<Content: View>: View { | |
var title: String | |
var headerGradient: Gradient | |
@ViewBuilder var content: () -> Content | |
private let headerHeight = (collapsed: 50.0, expanded: 150.0) | |
@State private var scrollOffset = CGPoint() | |
var body: some View { | |
GeometryReader { geometry in | |
OffsetObservingScrollView(offset: $scrollOffset) { | |
VStack(spacing: 0) { | |
makeHeaderText(collapsed: false) | |
content() | |
} | |
} | |
.overlay(alignment: .top) { | |
makeHeaderText(collapsed: true) | |
.background(alignment: .top) { | |
headerLinearGradient.ignoresSafeArea() | |
} | |
.opacity(collapsedHeaderOpacity) | |
} | |
.background(alignment: .top) { | |
// We attach the expanded header's background to the scroll | |
// view itself, so that we can make it expand into both the | |
// safe area, as well as any negative scroll offset area: | |
headerLinearGradient | |
.frame(height: max(0, headerHeight.expanded - scrollOffset.y) + geometry.safeAreaInsets.top) | |
.ignoresSafeArea() | |
} | |
} | |
} | |
} | |
private extension ContentView { | |
var collapsedHeaderOpacity: CGFloat { | |
let minOpacityOffset = headerHeight.expanded / 2 | |
let maxOpacityOffset = headerHeight.expanded - headerHeight.collapsed | |
guard scrollOffset.y > minOpacityOffset else { return 0 } | |
guard scrollOffset.y < maxOpacityOffset else { return 1 } | |
let opacityOffsetRange = maxOpacityOffset - minOpacityOffset | |
return (scrollOffset.y - minOpacityOffset) / opacityOffsetRange | |
} | |
var headerLinearGradient: LinearGradient { | |
LinearGradient( | |
gradient: headerGradient, | |
startPoint: .top, | |
endPoint: .bottom | |
) | |
} | |
func makeHeaderText(collapsed: Bool) -> some View { | |
Text(title) | |
.font(collapsed ? .body : .title) | |
.lineLimit(1) | |
.padding() | |
.frame(height: collapsed ? headerHeight.collapsed : headerHeight.expanded) | |
.frame(maxWidth: .infinity) | |
.foregroundColor(.white) | |
.accessibilityHeading(.h1) | |
.accessibilityHidden(collapsed) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment