Skip to content

Commit 096c4a9

Browse files
committed
Add a headerStretch parameter to the scroll view with sticky header
1 parent 03d1ae1 commit 096c4a9

File tree

7 files changed

+64
-55
lines changed

7 files changed

+64
-55
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ struct MyView: View {
6060
var body: some View {
6161
ScrollViewWithStickyHeader(
6262
header: stickyHeader, // A header view
63-
headerHeight: 250, // Its resting height
64-
headerMinHeight: 150, // Its minimum height
65-
onScroll: handleScroll // An optional scroll action
63+
headerHeight: 250, // The resting header height
64+
headerMinHeight: 150, // The minimum header height
65+
headerStretch: false, // Disables the stretch effect
66+
contentCornerRadius: 20 // An optional corner radius mask
67+
onScroll: handleScroll // An optional scroll handler action
6668
) {
6769
// Add your scroll content here, e.g. a `LazyVStack`
6870
}

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This version moves examples into a new namespace and renames some View extension
1212

1313
### ✨ New Features
1414

15+
* The `ScrollViewWithStickyHeader` has a new `headerStretch` parameter.
1516
* The `ScrollViewWithStickyHeader` has a new `contentCornerRadius` parameter.
1617
* The new `scrollViewContentWithHeaderOverlap(...)` view extension can apply a header overlap to a scroll view content view.
1718
* The new `scrollViewContentWithRoundedHeaderOverlap(...)` view extension can apply a rounded header overlap to a scroll view content view.

Sources/ScrollKit/Examples/Examples+SpotifyAlbumScreen.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public extension Examples.Spotify {
4040
header: scrollViewHeader,
4141
headerHeight: Examples.Spotify.AlbumScreen.Header.height,
4242
headerMinHeight: 50,
43+
headerStretch: false,
4344
contentCornerRadius: scrollContentCornerRadius,
4445
onScroll: handleScrollOffset
4546
) {
@@ -136,7 +137,7 @@ private struct Preview: View {
136137
#if os(iOS)
137138
#Preview("Sheet") {
138139

139-
struct Preview: View {
140+
struct SheetPreview: View {
140141

141142
@State var isPresented = false
142143

@@ -153,7 +154,7 @@ private struct Preview: View {
153154
}
154155
}
155156

156-
return Preview()
157+
return SheetPreview()
157158
}
158159
#endif
159160

Sources/ScrollKit/Examples/Examples+SpotifyAlbumScreenHeader.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public extension Examples.Spotify.AlbumScreen {
3737
cover
3838
.padding(.bottom, bottomPadding)
3939
}
40+
.clipped()
4041
}
4142
}
4243
}

Sources/ScrollKit/Extensions/View+RoundedScollContent.swift

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
// Copyright © 2025 Daniel Saidi. All rights reserved.
77
//
88

9-
#if os(iOS) || os(visionOS)
109
import SwiftUI
1110

1211
public extension View {
@@ -27,7 +26,7 @@ public extension View {
2726
}
2827
}
2928

30-
@available(iOS 16.0, *)
29+
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
3130
public extension View {
3231

3332
/// Make a scroll view's content view overlap the scroll
@@ -49,9 +48,6 @@ public extension View {
4948
.frame(maxHeight: .infinity)
5049
.scrollViewContentWithHeaderOverlap(overlap)
5150
}
52-
}
53-
54-
public extension View {
5551

5652
/// Make a scroll view header view apply rounded corners
5753
/// that cut out a mask for the scroll view content view.
@@ -62,34 +58,29 @@ public extension View {
6258
func scrollViewHeaderWithRoundedContentMask(
6359
_ points: Double = 0
6460
) -> some View {
65-
if #available(iOS 16.0, *) {
66-
if points > 0 {
67-
self.mask {
68-
VStack(spacing: 0) {
69-
/// Make the black color overflow waaaay up.
70-
Color.black.scaleEffect(100, anchor: .bottom)
71-
ZStack {
72-
Color.white
73-
UnevenRoundedRectangle(
74-
topLeadingRadius: 20,
75-
topTrailingRadius: 20
76-
)
77-
.fill(.black)
78-
}
79-
.compositingGroup()
80-
.luminanceToAlpha()
81-
.frame(height: 20)
61+
if points > 0 {
62+
self.mask {
63+
VStack(spacing: 0) {
64+
/// Make the black color overflow waaaay up.
65+
Color.black.scaleEffect(100, anchor: .bottom)
66+
ZStack {
67+
Color.white
68+
UnevenRoundedRectangle(
69+
topLeadingRadius: 20,
70+
topTrailingRadius: 20
71+
)
72+
.fill(.black)
8273
}
74+
.compositingGroup()
75+
.luminanceToAlpha()
76+
.frame(height: 20)
8377
}
84-
} else {
85-
self
8678
}
8779
} else {
8880
self
8981
}
9082
}
9183
}
92-
#endif
9384

9485
#Preview {
9586

Sources/ScrollKit/ScrollKit.docc/Articles/Getting-Started-Article.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ struct MyView: View {
8080
var body: some View {
8181
ScrollViewWithStickyHeader(
8282
header: stickyHeader, // A header view
83-
headerHeight: 250, // Its resting height
84-
headerMinHeight: 150, // Its minimum height
85-
onScroll: handleScroll // An optional scroll action
83+
headerHeight: 250, // The resting header height
84+
headerMinHeight: 150, // The minimum header height
85+
headerStretch: false, // Disables the stretch effect
86+
contentCornerRadius: 20 // An optional corner radius mask
87+
onScroll: handleScroll // An optional scroll handler action
8688
) {
8789
// Add your scroll content here, e.g. a `LazyVStack`
8890
}

Sources/ScrollKit/ScrollViewWithStickyHeader.swift

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ import SwiftUI
1717
/// a ``ScrollViewHeader`` to make your header view properly
1818
/// stretch out when the scroll view is pulled down.
1919
///
20-
/// You can apply a `headerHeight` which will be the initial
21-
/// resting height of your sticky header, a `headerMinHeight`
22-
/// as its minimum height (below the safe area inset), and a
20+
/// You can apply a `headerHeight` which will be the resting
21+
/// height of the header, a `headerMinHeight` as the minimum
22+
/// header height (below the top safe area), and an optional
2323
/// `contentCornerRadius` which applies a corner radius mask
24-
/// under which the content will scroll.
24+
/// under which the scroll view content will scroll. You can
25+
/// also set the `headerStretch` parameter to `false` if you
26+
/// prefer to disable the header stretch effect. This can be
27+
/// nice when the view is rendered in a sheet, where pulling
28+
/// down should dismiss the sheet rather than stretching the
29+
/// sticky header.
2530
///
2631
/// You can use the `onScroll` init parameter to pass in any
2732
/// function that should be called whenever the view scrolls.
@@ -47,6 +52,7 @@ public struct ScrollViewWithStickyHeader<Header: View, Content: View>: View {
4752
/// - header: The scroll view header builder.
4853
/// - headerHeight: The height to apply to the scroll view header.
4954
/// - headerMinHeight: The minimum height to apply to the scroll view header, by default the `headerHeight`.
55+
/// - headerStretch: Whether to stretch out the header when pulling down, by default `true`.
5056
/// - contentCornerRadius: The corner radius to apply to the scroll content.
5157
/// - showsIndicators: Whether or not to show scroll indicators, by default `true`.
5258
/// - onScroll: An action that will be called whenever the scroll offset changes, by default `nil`.
@@ -56,7 +62,8 @@ public struct ScrollViewWithStickyHeader<Header: View, Content: View>: View {
5662
@ViewBuilder header: @escaping () -> Header,
5763
headerHeight: Double,
5864
headerMinHeight: Double? = nil,
59-
contentCornerRadius: CGFloat? = nil,
65+
headerStretch: Bool = true,
66+
contentCornerRadius: CGFloat = 0,
6067
showsIndicators: Bool = true,
6168
onScroll: ScrollAction? = nil,
6269
@ViewBuilder content: @escaping () -> Content
@@ -66,6 +73,7 @@ public struct ScrollViewWithStickyHeader<Header: View, Content: View>: View {
6673
self.header = header
6774
self.headerHeight = headerHeight
6875
self.headerMinHeight = headerMinHeight ?? headerHeight
76+
self.headerStretch = headerStretch
6977
self.contentCornerRadius = contentCornerRadius
7078
self.onScroll = onScroll
7179
self.content = content
@@ -76,7 +84,8 @@ public struct ScrollViewWithStickyHeader<Header: View, Content: View>: View {
7684
private let header: () -> Header
7785
private let headerHeight: Double
7886
private let headerMinHeight: Double
79-
private let contentCornerRadius: CGFloat?
87+
private let headerStretch: Bool
88+
private let contentCornerRadius: CGFloat
8089
private let onScroll: ScrollAction?
8190
private let content: () -> Content
8291

@@ -86,7 +95,10 @@ public struct ScrollViewWithStickyHeader<Header: View, Content: View>: View {
8695
private var scrollOffset: CGPoint = .zero
8796

8897
private var visibleHeaderRatio: CGFloat {
89-
(headerHeight + scrollOffset.y) / headerHeight
98+
let value = (headerHeight + scrollOffset.y) / headerHeight
99+
if headerStretch { return value }
100+
print(value)
101+
return min(1, value)
90102
}
91103

92104
public var body: some View {
@@ -117,25 +129,15 @@ private extension ScrollViewWithStickyHeader {
117129
in geo: GeometryProxy
118130
) -> Bool {
119131
let minHeight = headerMinHeight(in: geo)
120-
let safe = geo.safeAreaInsets.top
121-
122-
print("---")
123-
print("True height: \(self.headerHeight)")
124-
print("True min: \(self.headerMinHeight)")
125-
print("Calculated min: \(minHeight)")
126-
print("Safe area: \(safe)")
127-
print("Offset: \(scrollOffset.y)")
128-
129-
return (scrollOffset.y + 5) < -minHeight
132+
return scrollOffset.y < -minHeight
130133
}
131134

132-
@ViewBuilder
133135
func navbarOverlay(
134136
in geo: GeometryProxy
135137
) -> some View {
136138
let minHeight = headerMinHeight(in: geo)
137139
let ratioHeight = headerHeight * visibleHeaderRatio
138-
Color.clear.overlay(alignment: .bottom) {
140+
return Color.clear.overlay(alignment: .bottom) {
139141
scrollHeader
140142
}
141143
.frame(height: max(minHeight, ratioHeight))
@@ -161,11 +163,19 @@ private extension ScrollViewWithStickyHeader {
161163

162164
@ViewBuilder
163165
var scrollHeader: some View {
164-
let radius = contentCornerRadius ?? 0
166+
if #available(iOS 16.0, *) {
167+
scrollHeaderView
168+
.scrollViewHeaderWithRoundedContentMask(contentCornerRadius)
169+
} else {
170+
scrollHeaderView
171+
}
172+
}
173+
174+
@ViewBuilder
175+
var scrollHeaderView: some View {
165176
ScrollViewHeader(content: header)
166177
.frame(minHeight: headerHeight)
167178
.edgesIgnoringSafeArea(.all)
168-
.scrollViewHeaderWithRoundedContentMask(radius)
169179
}
170180

171181
func handleScrollOffset(_ offset: CGPoint) {
@@ -217,7 +227,8 @@ private struct Preview: View {
217227
.vertical,
218228
header: header,
219229
headerHeight: 250,
220-
headerMinHeight: 50,
230+
headerMinHeight: 100,
231+
headerStretch: false,
221232
contentCornerRadius: contentCornerRadius,
222233
showsIndicators: false,
223234
onScroll: { offset, visibleHeaderRatio in

0 commit comments

Comments
 (0)