Created
March 22, 2024 22:26
-
-
Save eliottrobson/cbc456dc7f7a046ddb14585916a0df8f to your computer and use it in GitHub Desktop.
SwiftUI Sticky Header
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+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 | |
) | |
} | |
} |
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
// | |
// 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