Created
April 13, 2023 16:52
-
-
Save cjnevin/42a752f9bcc98ec9cfccdb21d1961102 to your computer and use it in GitHub Desktop.
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
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