Skip to content

Instantly share code, notes, and snippets.

@eliottrobson
Created March 22, 2024 22:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliottrobson/cbc456dc7f7a046ddb14585916a0df8f to your computer and use it in GitHub Desktop.
Save eliottrobson/cbc456dc7f7a046ddb14585916a0df8f to your computer and use it in GitHub Desktop.
SwiftUI Sticky Header
//
// View+PositionSticky.swift
// App
//
// Created by Eliott Robson on 22/03/2024.
//
import SwiftUI
extension View {
@ViewBuilder func stickyElement(_ sticky: ViewStickyData) -> some View {
self
.anchorPreference(key: ViewStickyElementPreferenceKey.self, value: .bounds, transform: {
ViewStickyElementPreferenceData(elementBounds: $0)
})
.offset(y: -sticky.offset)
}
@ViewBuilder func stickyContainer(_ sticky: ViewStickyData, _ geometry: GeometryProxy) -> some View {
self
.transformAnchorPreference(key: ViewStickyElementPreferenceKey.self, value: .bounds, transform: { ( value: inout ViewStickyElementPreferenceData, anchor: Anchor<CGRect>) in
value.containerBounds = anchor
})
.onPreferenceChange(ViewStickyElementPreferenceKey.self) { stickyData in
guard let elementBounds = stickyData.elementBounds,
let containerBounds = stickyData.containerBounds
else { return }
let top = geometry[containerBounds].minY
let elementHeight = geometry[elementBounds].height - Config.UX.spacing
let bottom = elementHeight - geometry[containerBounds].maxY
let newOffset = min(0, top) + max(0, bottom)
if sticky.offset != newOffset {
sticky.offset = newOffset
}
}
}
}
class ViewStickyData: ObservableObject {
@Published var offset: CGFloat
init(offset: CGFloat = .zero) {
self.offset = offset
}
}
struct ViewStickyElementPreferenceData: Equatable {
let elementBounds: Anchor<CGRect>?
var containerBounds: Anchor<CGRect>?
init(elementBounds: Anchor<CGRect>? = nil, containerBounds: Anchor<CGRect>? = nil) {
self.elementBounds = elementBounds
self.containerBounds = containerBounds
}
}
struct ViewStickyElementPreferenceKey: PreferenceKey {
static var defaultValue = ViewStickyElementPreferenceData()
static func reduce(value: inout ViewStickyElementPreferenceData, nextValue: () -> ViewStickyElementPreferenceData) {
let tempValue = nextValue()
value = ViewStickyElementPreferenceData(
elementBounds: tempValue.elementBounds ?? value.elementBounds,
containerBounds: tempValue.containerBounds ?? value.containerBounds
)
}
}
//
// ViewTest.swift
// App
//
// Created by Eliott Robson on 22/03/2024.
//
import SwiftUI
struct ViewTest: View {
@StateObject private var sticky = ViewStickyData()
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0) {
Text("Header")
.stickyElement(sticky)
ForEach(1...100, id: \.self) { count in
Text("Content \(count)")
}
}
.stickyContainer(sticky, geometry)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment