Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ let package = Package(
name: "ScrollTracking",
targets: ["ScrollTracking"]
),
.library(
name: "StickyHeader",
targets: ["StickyHeader"]
),
],
dependencies: [
.package(url: "https://github.com/FluidGroup/swift-indexed-collection", from: "0.2.1"),
Expand Down Expand Up @@ -48,6 +52,11 @@ let package = Package(
.product(name: "WithPrerender", package: "swift-with-prerender"),
]
),
.target(
name: "StickyHeader",
dependencies: [
]
),
.testTarget(
name: "DynamicListTests",
dependencies: ["DynamicList"]
Expand Down
208 changes: 208 additions & 0 deletions Sources/StickyHeader/StickyHeader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import SwiftUI

/**
A view that sticks to the top of the screen in a ScrollView.
When it's bouncing, it stretches the content.
To use this view, you need to call ``View.enableStickyHeader()`` modifier to the ScrollView.
*/
public struct StickyHeader<Content: View>: View {

/**
The option to determine how to size the header.
*/
public enum Sizing {
/// Uses the given content's intrinsic size.
case content
/// Uses the fixed height.
case fixed(CGFloat)
}

public let sizing: Sizing
public let content: Content

@State var baseContentHeight: CGFloat?
@State var stretchingValue: CGFloat = 0

public init(
sizing: Sizing,
@ViewBuilder content: () -> Content
) {
self.sizing = sizing
self.content = content()
}

public var body: some View {

let offsetY: CGFloat = 0

Group {
switch sizing {
case .content:
content
.onGeometryChange(for: CGSize.self, of: \.size) { size in
if stretchingValue == 0 {
baseContentHeight = size.height
}
}
.frame(height: baseContentHeight.map {
$0 + stretchingValue
})
.offset(y: -stretchingValue)
// container
.frame(height: baseContentHeight, alignment: .top)

case .fixed(let height):

content
.frame(height: height + stretchingValue + offsetY)
.offset(y: -offsetY)
.offset(y: -stretchingValue)
// container
.frame(height: height, alignment: .top)
}
}
.onGeometryChange(
for: CGRect.self,
of: {
$0.frame(in: .named(coordinateSpaceName))
},
action: { value in
self.stretchingValue = max(0, value.minY)
})

}
}

private let coordinateSpaceName = "app.muukii.stickyHeader.scrollView"

extension View {

public func enableStickyHeader() -> some View {
coordinateSpace(name: coordinateSpaceName)
}

}

#Preview("dynamic") {
ScrollView {

StickyHeader(sizing: .content) {

ZStack {

Color.red
.padding(.top, -100)

VStack {
Text("StickyHeader")
Text("StickyHeader")
Text("StickyHeader")
}
.border(Color.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
// .background(.yellow)
}

}

ForEach(0..<100, id: \.self) { _ in
Text("Hello World!")
.frame(maxWidth: .infinity)
}
}
.enableStickyHeader()
.padding(.vertical, 100)
}

#Preview("dynamic full") {
ScrollView {

StickyHeader(sizing: .content) {

ZStack {

Color.red

VStack {
Text("StickyHeader")
Text("StickyHeader")
Text("StickyHeader")
}
.border(Color.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.yellow)
.background(
Color.green
.padding(.top, -100)

)
}

}

ForEach(0..<100, id: \.self) { _ in
Text("Hello World!")
.frame(maxWidth: .infinity)
}
}
.enableStickyHeader()
}

#Preview("fixed") {
ScrollView {

StickyHeader(sizing: .fixed(300)) {

Rectangle()
.stroke(lineWidth: 10)
.overlay(
VStack {
Text("StickyHeader")
Text("StickyHeader")
Text("StickyHeader")
}
)
}

ForEach(0..<100, id: \.self) { _ in
Text("Hello World!")
.frame(maxWidth: .infinity)
}
}
.enableStickyHeader()
.padding(.vertical, 100)
}

#Preview("fixed full") {
ScrollView {

StickyHeader(sizing: .fixed(300)) {

ZStack {

Color.red

VStack {
Text("StickyHeader")
Text("StickyHeader")
Text("StickyHeader")
}
.border(Color.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
// .background(.yellow)
.background(
Color.green
.padding(.top, -100)

)
}
}

ForEach(0..<100, id: \.self) { _ in
Text("Hello World!")
.frame(maxWidth: .infinity)
}
}
.enableStickyHeader()

}