Skip to content

Commit ad84b81

Browse files
authored
StickyHeader for scrollview (#48)
1 parent b879839 commit ad84b81

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

Package.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ let package = Package(
2020
name: "ScrollTracking",
2121
targets: ["ScrollTracking"]
2222
),
23+
.library(
24+
name: "StickyHeader",
25+
targets: ["StickyHeader"]
26+
),
2327
],
2428
dependencies: [
2529
.package(url: "https://github.com/FluidGroup/swift-indexed-collection", from: "0.2.1"),
@@ -48,6 +52,11 @@ let package = Package(
4852
.product(name: "WithPrerender", package: "swift-with-prerender"),
4953
]
5054
),
55+
.target(
56+
name: "StickyHeader",
57+
dependencies: [
58+
]
59+
),
5160
.testTarget(
5261
name: "DynamicListTests",
5362
dependencies: ["DynamicList"]
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import SwiftUI
2+
3+
/**
4+
A view that sticks to the top of the screen in a ScrollView.
5+
When it's bouncing, it stretches the content.
6+
To use this view, you need to call ``View.enableStickyHeader()`` modifier to the ScrollView.
7+
*/
8+
public struct StickyHeader<Content: View>: View {
9+
10+
/**
11+
The option to determine how to size the header.
12+
*/
13+
public enum Sizing {
14+
/// Uses the given content's intrinsic size.
15+
case content
16+
/// Uses the fixed height.
17+
case fixed(CGFloat)
18+
}
19+
20+
public let sizing: Sizing
21+
public let content: Content
22+
23+
@State var baseContentHeight: CGFloat?
24+
@State var stretchingValue: CGFloat = 0
25+
26+
public init(
27+
sizing: Sizing,
28+
@ViewBuilder content: () -> Content
29+
) {
30+
self.sizing = sizing
31+
self.content = content()
32+
}
33+
34+
public var body: some View {
35+
36+
let offsetY: CGFloat = 0
37+
38+
Group {
39+
switch sizing {
40+
case .content:
41+
content
42+
.onGeometryChange(for: CGSize.self, of: \.size) { size in
43+
if stretchingValue == 0 {
44+
baseContentHeight = size.height
45+
}
46+
}
47+
.frame(height: baseContentHeight.map {
48+
$0 + stretchingValue
49+
})
50+
.offset(y: -stretchingValue)
51+
// container
52+
.frame(height: baseContentHeight, alignment: .top)
53+
54+
case .fixed(let height):
55+
56+
content
57+
.frame(height: height + stretchingValue + offsetY)
58+
.offset(y: -offsetY)
59+
.offset(y: -stretchingValue)
60+
// container
61+
.frame(height: height, alignment: .top)
62+
}
63+
}
64+
.onGeometryChange(
65+
for: CGRect.self,
66+
of: {
67+
$0.frame(in: .named(coordinateSpaceName))
68+
},
69+
action: { value in
70+
self.stretchingValue = max(0, value.minY)
71+
})
72+
73+
}
74+
}
75+
76+
private let coordinateSpaceName = "app.muukii.stickyHeader.scrollView"
77+
78+
extension View {
79+
80+
public func enableStickyHeader() -> some View {
81+
coordinateSpace(name: coordinateSpaceName)
82+
}
83+
84+
}
85+
86+
#Preview("dynamic") {
87+
ScrollView {
88+
89+
StickyHeader(sizing: .content) {
90+
91+
ZStack {
92+
93+
Color.red
94+
.padding(.top, -100)
95+
96+
VStack {
97+
Text("StickyHeader")
98+
Text("StickyHeader")
99+
Text("StickyHeader")
100+
}
101+
.border(Color.red)
102+
.frame(maxWidth: .infinity, maxHeight: .infinity)
103+
// .background(.yellow)
104+
}
105+
106+
}
107+
108+
ForEach(0..<100, id: \.self) { _ in
109+
Text("Hello World!")
110+
.frame(maxWidth: .infinity)
111+
}
112+
}
113+
.enableStickyHeader()
114+
.padding(.vertical, 100)
115+
}
116+
117+
#Preview("dynamic full") {
118+
ScrollView {
119+
120+
StickyHeader(sizing: .content) {
121+
122+
ZStack {
123+
124+
Color.red
125+
126+
VStack {
127+
Text("StickyHeader")
128+
Text("StickyHeader")
129+
Text("StickyHeader")
130+
}
131+
.border(Color.red)
132+
.frame(maxWidth: .infinity, maxHeight: .infinity)
133+
.background(.yellow)
134+
.background(
135+
Color.green
136+
.padding(.top, -100)
137+
138+
)
139+
}
140+
141+
}
142+
143+
ForEach(0..<100, id: \.self) { _ in
144+
Text("Hello World!")
145+
.frame(maxWidth: .infinity)
146+
}
147+
}
148+
.enableStickyHeader()
149+
}
150+
151+
#Preview("fixed") {
152+
ScrollView {
153+
154+
StickyHeader(sizing: .fixed(300)) {
155+
156+
Rectangle()
157+
.stroke(lineWidth: 10)
158+
.overlay(
159+
VStack {
160+
Text("StickyHeader")
161+
Text("StickyHeader")
162+
Text("StickyHeader")
163+
}
164+
)
165+
}
166+
167+
ForEach(0..<100, id: \.self) { _ in
168+
Text("Hello World!")
169+
.frame(maxWidth: .infinity)
170+
}
171+
}
172+
.enableStickyHeader()
173+
.padding(.vertical, 100)
174+
}
175+
176+
#Preview("fixed full") {
177+
ScrollView {
178+
179+
StickyHeader(sizing: .fixed(300)) {
180+
181+
ZStack {
182+
183+
Color.red
184+
185+
VStack {
186+
Text("StickyHeader")
187+
Text("StickyHeader")
188+
Text("StickyHeader")
189+
}
190+
.border(Color.red)
191+
.frame(maxWidth: .infinity, maxHeight: .infinity)
192+
// .background(.yellow)
193+
.background(
194+
Color.green
195+
.padding(.top, -100)
196+
197+
)
198+
}
199+
}
200+
201+
ForEach(0..<100, id: \.self) { _ in
202+
Text("Hello World!")
203+
.frame(maxWidth: .infinity)
204+
}
205+
}
206+
.enableStickyHeader()
207+
208+
}

0 commit comments

Comments
 (0)