Skip to content

Instantly share code, notes, and snippets.

@cjnevin
Created April 13, 2023 16:52
Show Gist options
  • Save cjnevin/42a752f9bcc98ec9cfccdb21d1961102 to your computer and use it in GitHub Desktop.
Save cjnevin/42a752f9bcc98ec9cfccdb21d1961102 to your computer and use it in GitHub Desktop.
import SwiftUI
enum Constants {
static let heroHeight: CGFloat = 500
static let menuHeight: CGFloat = 150
}
struct LargeHeaderView: View {
let color: Color
var body: some View {
ZStack {
Rectangle().fill(color)
Text("Center")
}
}
}
struct PinningScrollView: View {
struct Item: Identifiable {
enum Behaviour {
case pinnedBelow
case normal
case pinnedAbove
}
let id: String = UUID().uuidString
let behaviour: Behaviour
let height: CGFloat
let color: Color
}
@State var items: [Item] = [
.init(behaviour: .pinnedBelow, height: Constants.heroHeight, color: .red),
.init(behaviour: .pinnedBelow, height: Constants.heroHeight, color: .blue),
.init(behaviour: .pinnedAbove, height: Constants.menuHeight, color: .green),
.init(behaviour: .pinnedBelow, height: Constants.heroHeight, color: .yellow),
.init(behaviour: .normal, height: Constants.heroHeight, color: .purple),
]
func alpha(id: String, reader: GeometryProxy) -> CGFloat {
guard let index = items.firstIndex(where: { $0.id == id }) else {
return 0
}
guard items[index].behaviour == .pinnedBelow else {
return 1.0
}
let scrollPosition = reader.frame(in: .global).minY
let y = items[0..<index].reduce(0) { $0 + $1.height }
let itemHeight = items[index].height
let absolute = abs(scrollPosition)
// Only fade current item
if scrollPosition < 0, absolute > y && absolute < y + itemHeight {
let relativeOffset = absolute - y
return 1.0 - (relativeOffset / itemHeight)
}
return 1.0
}
func offset(id: String, reader: GeometryProxy) -> CGFloat {
guard let index = items.firstIndex(where: { $0.id == id }) else {
return 0
}
let scrollPosition = reader.frame(in: .global).minY
let absolute = abs(scrollPosition)
let y = items[0..<index].reduce(0) { $0 + $1.height }
switch items[index].behaviour {
case .pinnedBelow:
// Only change offset of current item
if scrollPosition < 0, absolute > y && absolute < y + items[index].height {
return absolute
}
return y
case .normal:
return y
case .pinnedAbove:
if scrollPosition < 0, absolute > y {
return absolute
}
return y
}
}
func zIndex(id: String) -> Double {
guard let index = items.firstIndex(where: { $0.id == id }) else {
return 0
}
switch items[index].behaviour {
case .pinnedBelow, .normal: return Double(index)
case .pinnedAbove: return Double(index + 100)
}
}
var body: some View {
ScrollView {
GeometryReader { reader in
ForEach(items) { item in
LargeHeaderView(color: item.color)
.id(item.id)
.zIndex(zIndex(id: item.id))
.opacity(alpha(id: item.id, reader: reader))
.offset(y: offset(id: item.id, reader: reader))
.frame(height: item.height)
}
}
.frame(height: items.reduce(0, { value, item in value + item.height }))
}
.scrollIndicators(.hidden)
.ignoresSafeArea()
.background(Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
PinningScrollView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment