Skip to content

Instantly share code, notes, and snippets.

@cjnevin
Last active April 13, 2023 18:16
Show Gist options
  • Save cjnevin/d295e04df769b976770b2c356d1094c3 to your computer and use it in GitHub Desktop.
Save cjnevin/d295e04df769b976770b2c356d1094c3 to your computer and use it in GitHub Desktop.
import SwiftUI
enum Constants {
static let backgroundHeight: CGFloat = 500
static let headerHeight: CGFloat = 150
static let contentHeight: CGFloat = 350
}
struct ColoredView: View {
let color: Color
var body: some View {
ZStack {
Rectangle().fill(color)
Text("Center")
}
}
}
struct Header: Identifiable {
let id: String
var items: [HeaderItem]
}
struct HeaderItem: Identifiable {
let id: String
let text: String
var selected: Bool
}
struct HeaderItemView: View {
@Binding var item: HeaderItem
let onTap: () -> Void
var body: some View {
Button(item.text) {
onTap()
}
.padding()
.background(item.selected ? .blue : .init(white: 0.1))
.foregroundColor(item.selected ? .init(white: 0.1) : .blue)
.cornerRadius(10)
}
}
struct HeaderView: View {
@Binding var header: Header
var body: some View {
ScrollView(.horizontal) {
LazyHStack(spacing: 10) {
ForEach($header.items) { item in
HeaderItemView(item: item) {
var mutableHeader = header
for index in mutableHeader.items.indices {
mutableHeader.items[index].selected = mutableHeader.items[index].id == item.id
}
header = mutableHeader
}
}
}
}
.padding()
.background(Color.black)
}
}
struct PinModifier: ViewModifier {
enum Behaviour {
case below
case none
case above
}
struct Item {
let behaviour: Behaviour
let height: CGFloat
}
let index: Int
let items: [Item]
func opacity(reader: GeometryProxy) -> CGFloat {
guard items[index].behaviour == .below 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(reader: GeometryProxy) -> CGFloat {
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 .below:
// Only change offset of current item
if scrollPosition < 0, absolute > y && absolute < y + items[index].height {
return absolute
}
return y
case .none:
return y
case .above:
if scrollPosition < 0, absolute > y {
return absolute
}
return y
}
}
func zIndex() -> Double {
switch items[index].behaviour {
case .below, .none: return Double(index)
case .above: return Double(index + 100)
}
}
func body(content: Content) -> some View {
GeometryReader { reader in
content
.opacity(opacity(reader: reader))
.offset(y: offset(reader: reader))
}
.zIndex(zIndex())
.frame(height: items[index].height)
}
}
struct PinningScrollView: View {
struct BackgroundItem: Identifiable {
let id: String
let color: Color
}
struct ContentItem: Identifiable {
let id: String
let color: Color
}
enum Item: Identifiable {
case background(BackgroundItem)
case content(ContentItem)
case header(Header)
var id: String {
switch self {
case .background(let item): return item.id
case .content(let item): return item.id
case .header(let item): return item.id
}
}
var pinItem: PinModifier.Item {
switch self {
case .background: return .init(behaviour: .below, height: Constants.backgroundHeight)
case .content: return .init(behaviour: .none, height: Constants.contentHeight)
case .header: return .init(behaviour: .above, height: Constants.headerHeight)
}
}
}
@State var items: [Item] = [
.background(.init(id: "1", color: .red)),
.background(.init(id: "2", color: .blue)),
.header(.init(id: "3", items: [
.init(id: "1", text: "Item 1", selected: true),
.init(id: "2", text: "Item 2", selected: false),
.init(id: "3", text: "Item 3", selected: false),
.init(id: "4", text: "Item 4", selected: false),
])),
.content(.init(id: "4", color: .yellow)),
.background(.init(id: "6", color: .purple)),
.content(.init(id: "7", color: .pink))
]
var body: some View {
ScrollView {
GeometryReader { reader in
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
ZStack {
switch item {
case .background(let backgroundItem):
ColoredView(color: backgroundItem.color)
case .content(let contentItem):
ColoredView(color: contentItem.color)
case .header(let header):
HeaderView(header: .init(get: {
header
}, set: { newHeader in
items[index] = .header(newHeader)
print(newHeader.items.map(\.selected))
}))
}
}
.pin(
at: index,
in: items.map(\.pinItem)
)
}
}
.frame(height: items.map(\.pinItem.height).reduce(0, +))
}
.scrollIndicators(.hidden)
.ignoresSafeArea()
.background(Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
PinningScrollView()
}
}
extension View {
func pin(at index: Int, in items: [PinModifier.Item]) -> some View {
modifier(PinModifier(index: index, items: items))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment