Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Last active June 29, 2023 18:00
Show Gist options
  • Save Codelaby/b20a7b40dbef505f1f28cf6086d006c3 to your computer and use it in GitHub Desktop.
Save Codelaby/b20a7b40dbef505f1f28cf6086d006c3 to your computer and use it in GitHub Desktop.
Headers Scroll Swift
//
// TopImageHeader.swift
// Avatars
//
// Created by Codelaby on 28/6/23.
//
import SwiftUI
/*https://www.bilibili.com/video/BV1vV4y1k7LY/
https://www.swiftbysundell.com/articles/observing-swiftui-scrollview-content-offset/
https://danielsaidi.com/blog/2023/02/09/adding-a-sticky-header-to-a-swiftui-scroll-view
*/
let loremIpsum = """
Lorem ipsum dolor sit amet consectetur adipiscing elit donec, gravida commodo hac non mattis augue duis vitae inceptos, laoreet taciti at vehicula cum arcu dictum. Cras netus vivamus sociis pulvinar est erat, quisque imperdiet velit a justo maecenas, pretium gravida ut himenaeos nam. Tellus quis libero sociis class nec hendrerit, id proin facilisis praesent bibendum vehicula tristique, fringilla augue vitae primis turpis.
Sagittis vivamus sem morbi nam mattis phasellus vehicula facilisis suscipit posuere metus, iaculis vestibulum viverra nisl ullamcorper lectus curabitur himenaeos dictumst malesuada tempor, cras maecenas enim est eu turpis hac sociosqu tellus magnis. Sociosqu varius feugiat volutpat justo fames magna malesuada, viverra neque nibh parturient eu nascetur, cursus sollicitudin placerat lobortis nunc imperdiet. Leo lectus euismod morbi placerat pretium aliquet ultricies metus, augue turpis vulputa
te dictumst mattis egestas laoreet, cubilia habitant magnis lacinia vivamus etiam aenean.
"""
extension CGFloat {
func rounded(toPlaces places: Int) -> CGFloat {
let divisor = pow(10.0, CGFloat(places))
return (self * divisor).rounded() / divisor
}
}
extension View {
func scrimOverlay(startPoint: UnitPoint = .top, endPoint: UnitPoint = .bottom, colorStart: Color = .black, colorEnd: Color = .clear) -> some View {
self.overlay(
LinearGradient(
gradient: Gradient(
colors: [
colorStart.opacity(0.8),
colorStart.opacity(0.4),
colorStart.opacity(0.3),
colorEnd.opacity(0.1),
colorEnd
]
),
startPoint: startPoint,
endPoint: endPoint
)
)
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct TopImageHeader: View {
@State private var offset = CGFloat.zero
func getPercentHeight(maxHeight: CGFloat, height: CGFloat) -> CGFloat {
if height > 0 {
return 0
} else if height < -maxHeight {
return 1
} else {
let r = abs(-height / maxHeight)
return r.rounded(toPlaces: 2)
}
}
private func hasHeaderCollapsed() -> Bool {
return getPercentHeight(maxHeight: 200, height: offset) > 0.2
}
var body: some View {
ZStack(alignment: .topLeading) {
if (hasHeaderCollapsed()) {
TopView()
.opacity(getPercentHeight(maxHeight: 200, height: offset))
.zIndex(1)
}
ScrollView(.vertical) {
HeaderView()
//.zIndex(10)
ZStack() {
VStack(alignment: .leading, spacing: 16) {
Text("Content")
Text(loremIpsum)
Text(loremIpsum)
Text(loremIpsum)
}
.font(.body)
.padding()
}
.padding(.horizontal,2)
.background(Color(UIColor.systemBackground))
//.navigationBarItems(leading: EditButton())
}
.edgesIgnoringSafeArea(.top)
.onPreferenceChange(ViewOffsetKey.self) {
offset = $0
print("offset >> \($0) percentatge \(getPercentHeight(maxHeight: 200, height: offset))")
}
}
//.toolbarBackground(offset > -200 ? .hidden : .visible, for: .navigationBar)
}
private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat {
geometry.frame(in: .global).minY
}
private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
// Image was pulled down
if offset > 0 {
return -offset
}
return 0
}
private func getOffsetForHeaderImage2(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let headerHeight: CGFloat = 300
let minHeight: CGFloat = 72
let sizeOffScreen: CGFloat = headerHeight - minHeight
if offset < -sizeOffScreen {
let imageOffset = abs(min(-sizeOffScreen, offset))
return imageOffset - sizeOffScreen
}
if offset > 0 {
return -offset
}
return 0
}
private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let imageHeight = geometry.size.height
if offset > 0 {
return imageHeight + offset
}
return imageHeight
}
private func getBlurRadiusForImage(_ geometry: GeometryProxy) -> CGFloat {
// 2
let offset = geometry.frame(in: .global).maxY
let height = geometry.size.height
let blur = (height - max(offset, 0)) / height // 3 (values will range from 0 - 1)
return blur * 6 // Values will range from 0 - 6
}
@ViewBuilder
func HeaderView() -> some View {
GeometryReader { geometry in
let offset = geometry.frame(in: .global).minY.rounded(toPlaces: 2)
let parallaxFactor = offset * 0.4
//let offset2 = geometry.frame(in: .named("scroll")).minY
Color.clear.preference(key: ViewOffsetKey.self, value: offset)
Image("AvatarGirl")
.resizable()
.scaledToFill()
//.frame(width: geometry.size.width, height: geometry.size.height)
.frame(width: geometry.size.width, height: (self.getHeightForHeaderImage(geometry)))
.scrimOverlay()
//.scrimOverlay(startPoint: .top, endPoint: .bottom, colorStart: .black, colorEnd: Color(UIColor.systemBackground))
.blur(radius: self.getBlurRadiusForImage(geometry))
.clipped()
.offset(x: 0, y: (self.getOffsetForHeaderImage(geometry) - parallaxFactor) )
}
.frame(height: 270)
}
@ViewBuilder
func TopView() -> some View {
HStack() {
Text("Sticky header")
}
.frame(maxWidth: .infinity)
.padding(.horizontal, 16)
.padding(.bottom, 8)
.background(.regularMaterial)
//.padding(.top, 15)
}
}
struct TopImageHeader_Previews: PreviewProvider {
static var previews: some View {
TopImageHeader()
}
}
//
// TopImageHeader.swift
// Avatars
//
// Created by Codelaby on 28/6/23.
//
import SwiftUI
/*https://www.bilibili.com/video/BV1vV4y1k7LY/
https://www.swiftbysundell.com/articles/observing-swiftui-scrollview-content-offset/
*/
let loremIpsum = """
Lorem ipsum dolor sit amet consectetur adipiscing elit donec, gravida commodo hac non mattis augue duis vitae inceptos, laoreet taciti at vehicula cum arcu dictum. Cras netus vivamus sociis pulvinar est erat, quisque imperdiet velit a justo maecenas, pretium gravida ut himenaeos nam. Tellus quis libero sociis class nec hendrerit, id proin facilisis praesent bibendum vehicula tristique, fringilla augue vitae primis turpis.
Sagittis vivamus sem morbi nam mattis phasellus vehicula facilisis suscipit posuere metus, iaculis vestibulum viverra nisl ullamcorper lectus curabitur himenaeos dictumst malesuada tempor, cras maecenas enim est eu turpis hac sociosqu tellus magnis. Sociosqu varius feugiat volutpat justo fames magna malesuada, viverra neque nibh parturient eu nascetur, cursus sollicitudin placerat lobortis nunc imperdiet. Leo lectus euismod morbi placerat pretium aliquet ultricies metus, augue turpis vulputa
te dictumst mattis egestas laoreet, cubilia habitant magnis lacinia vivamus etiam aenean.
"""
struct TopImageHeader: View {
var body: some View {
ScrollView(.vertical) {
HeaderView()
VStack(alignment: .leading, spacing: 16) {
Text("Content")
Text(loremIpsum)
Text(loremIpsum)
Text(loremIpsum)
}
.padding()
}
.edgesIgnoringSafeArea(.top)
}
private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat {
geometry.frame(in: .global).minY
}
private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
// Image was pulled down
if offset > 0 {
return -offset
}
return 0
}
private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let imageHeight = geometry.size.height
if offset > 0 {
return imageHeight + offset
}
return imageHeight
}
@ViewBuilder
func HeaderView() -> some View {
GeometryReader { geometry in
Image("AvatarGirl")
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: self.getHeightForHeaderImage(geometry))
.clipped()
.offset(x: 0, y: self.getOffsetForHeaderImage(geometry)) // 3
}
.frame(height: 290)
}
}
struct TopImageHeader_Previews: PreviewProvider {
static var previews: some View {
TopImageHeader()
}
}
//
// TopImageHeader.swift
// Avatars
//
// Created by Codelaby on 28/6/23.
//
import SwiftUI
/*https://www.bilibili.com/video/BV1vV4y1k7LY/
https://www.swiftbysundell.com/articles/observing-swiftui-scrollview-content-offset/
https://danielsaidi.com/blog/2023/02/09/adding-a-sticky-header-to-a-swiftui-scroll-view
*/
let loremIpsum = """
Lorem ipsum dolor sit amet consectetur adipiscing elit donec, gravida commodo hac non mattis augue duis vitae inceptos, laoreet taciti at vehicula cum arcu dictum. Cras netus vivamus sociis pulvinar est erat, quisque imperdiet velit a justo maecenas, pretium gravida ut himenaeos nam. Tellus quis libero sociis class nec hendrerit, id proin facilisis praesent bibendum vehicula tristique, fringilla augue vitae primis turpis.
Sagittis vivamus sem morbi nam mattis phasellus vehicula facilisis suscipit posuere metus, iaculis vestibulum viverra nisl ullamcorper lectus curabitur himenaeos dictumst malesuada tempor, cras maecenas enim est eu turpis hac sociosqu tellus magnis. Sociosqu varius feugiat volutpat justo fames magna malesuada, viverra neque nibh parturient eu nascetur, cursus sollicitudin placerat lobortis nunc imperdiet. Leo lectus euismod morbi placerat pretium aliquet ultricies metus, augue turpis vulputa
te dictumst mattis egestas laoreet, cubilia habitant magnis lacinia vivamus etiam aenean.
"""
extension CGFloat {
func rounded(toPlaces places: Int) -> CGFloat {
let divisor = pow(10.0, CGFloat(places))
return (self * divisor).rounded() / divisor
}
}
extension View {
func scrimOverlay(startPoint: UnitPoint = .top, endPoint: UnitPoint = .bottom, colorStart: Color = .black, colorEnd: Color = .clear) -> some View {
self.overlay(
LinearGradient(
gradient: Gradient(
colors: [
colorStart.opacity(0.8),
colorStart.opacity(0.4),
colorStart.opacity(0.3),
colorEnd.opacity(0.1),
colorEnd
]
),
startPoint: startPoint,
endPoint: endPoint
)
)
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct TopImageHeader: View {
@State private var offset = CGFloat.zero
var body: some View {
NavigationStack() {
ScrollView(.vertical) {
HeaderView()
//.zIndex(10)
ZStack() {
VStack(alignment: .leading, spacing: 16) {
Text("Content")
Text(loremIpsum)
Text(loremIpsum)
Text(loremIpsum)
}
.font(.body)
.padding()
}
.padding(.horizontal,2)
.background(Color(UIColor.systemBackground))
.navigationBarItems(leading: EditButton())
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ViewOffsetKey.self) {
offset = $0
print("offset >> \($0)") }
.toolbarBackground(offset > -200 ? .hidden : .visible, for: .navigationBar)
}
.edgesIgnoringSafeArea(.top)
}
private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat {
geometry.frame(in: .global).minY
}
private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
// Image was pulled down
if offset > 0 {
return -offset
}
return 0
}
private func getOffsetForHeaderImage2(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let headerHeight: CGFloat = 300
let minHeight: CGFloat = 72
let sizeOffScreen: CGFloat = headerHeight - minHeight
if offset < -sizeOffScreen {
let imageOffset = abs(min(-sizeOffScreen, offset))
return imageOffset - sizeOffScreen
}
if offset > 0 {
return -offset
}
return 0
}
private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let imageHeight = geometry.size.height
if offset > 0 {
return imageHeight + offset
}
return imageHeight
}
private func getBlurRadiusForImage(_ geometry: GeometryProxy) -> CGFloat {
// 2
let offset = geometry.frame(in: .global).maxY
let height = geometry.size.height
let blur = (height - max(offset, 0)) / height // 3 (values will range from 0 - 1)
return blur * 6 // Values will range from 0 - 6
}
@ViewBuilder
func HeaderView() -> some View {
GeometryReader { geometry in
let offset = geometry.frame(in: .global).minY.rounded(toPlaces: 2)
let parallaxFactor = offset * 0.4
let offset2 = geometry.frame(in: .named("scroll")).minY
Color.clear.preference(key: ViewOffsetKey.self, value: offset)
Image("AvatarGirl")
.resizable()
.scaledToFill()
//.frame(width: geometry.size.width, height: geometry.size.height)
.frame(width: geometry.size.width, height: (self.getHeightForHeaderImage(geometry)))
.scrimOverlay()
//.scrimOverlay(startPoint: .top, endPoint: .bottom, colorStart: .black, colorEnd: Color(UIColor.systemBackground))
.blur(radius: self.getBlurRadiusForImage(geometry))
.clipped()
.offset(x: 0, y: (self.getOffsetForHeaderImage(geometry) - parallaxFactor) )
}
.frame(height: 290)
}
@ViewBuilder
func TopView() -> some View {
Text("Sticky header")
}
}
struct TopImageHeader_Previews: PreviewProvider {
static var previews: some View {
TopImageHeader()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment