Last active
June 29, 2023 18:00
-
-
Save Codelaby/b20a7b40dbef505f1f28cf6086d006c3 to your computer and use it in GitHub Desktop.
Headers Scroll Swift
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
// | |
// 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() | |
} | |
} |
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
// | |
// 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() | |
} | |
} |
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
// | |
// 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