Last active
June 19, 2023 16:37
-
-
Save leojquinteros/4cf56f12f8e7c59711269c7a3f5e1328 to your computer and use it in GitHub Desktop.
Just playing around with a collapsable header using SwiftUI
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
// | |
// CollapsableHeader.swift | |
// | |
// Created by Leo Quinteros on 25/06/22. | |
// | |
import SwiftUI | |
struct Message: Identifiable { | |
var id = UUID().uuidString | |
var message, username: String | |
var tintColor: Color | |
} | |
let messageText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" | |
let usernameText = "Lorem ipsum" | |
var allMessages: [Message] { | |
[Message](repeating: .init(message: messageText, username: usernameText, tintColor: .primary), count: 30) | |
} | |
struct OffsetModifier: ViewModifier { | |
@Binding var offset: CGFloat | |
func body(content: Content) -> some View { | |
content | |
.overlay( | |
GeometryReader { proxy -> Color in | |
let minY = proxy.frame(in: .named("scroll")).minY | |
DispatchQueue.main.async { | |
self.offset = minY | |
} | |
return Color.clear | |
}, | |
alignment: .top | |
) | |
} | |
} | |
struct CustomCorner: Shape { | |
var corners: UIRectCorner | |
var radius: CGFloat | |
func path(in rect: CGRect) -> Path { | |
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) | |
return Path(path.cgPath) | |
} | |
} | |
struct HomeView: View { | |
let maxHeight = UIScreen.main.bounds.height / 2.3 | |
var topEdge: CGFloat | |
@State var offset: CGFloat = 0 | |
var headerHeight: CGFloat { | |
let topHeight = maxHeight + offset | |
return topHeight > 80 + topEdge ? topHeight : 80 + topEdge | |
} | |
var cornerRadius: CGFloat { | |
let progress = -offset / (maxHeight - (80 + topEdge)) | |
let radius = (1 - progress) * 50 | |
return offset < 0 ? radius : 50 | |
} | |
var body: some View { | |
ScrollView(.vertical, showsIndicators: false) { | |
VStack(alignment: .leading, spacing: 15) { | |
GeometryReader { proxy in | |
TopBar(topEdge: topEdge, offset: $offset, maxHeight: maxHeight) | |
.foregroundColor(.white) | |
.frame(maxWidth: .infinity) | |
.frame(height: headerHeight, alignment: .bottom) | |
.background( | |
Color.blue, in: CustomCorner(corners: [.bottomRight], radius: cornerRadius) | |
) | |
.overlay(NavBar(topEdge: topEdge, offset: $offset, maxHeight: maxHeight), alignment: .top) | |
} | |
.frame(height: maxHeight) | |
.offset(y: -offset) | |
.zIndex(1) | |
VStack(spacing: 15) { | |
ForEach(allMessages) { message in | |
MessageCardView(message: message) | |
} | |
} | |
.padding() | |
.zIndex(0) | |
} | |
.modifier(OffsetModifier(offset: $offset)) | |
} | |
.coordinateSpace(name: "scroll") | |
} | |
} | |
struct NavBar: View { | |
let topEdge: CGFloat | |
@Binding var offset: CGFloat | |
var maxHeight: CGFloat | |
var topBarOpacity: CGFloat { | |
-(offset + 70) / (maxHeight - (80 + topEdge)) | |
} | |
var body: some View { | |
HStack(alignment: .center, spacing: 15) { | |
Button(action: { | |
}, label: { | |
Image(systemName: "xmark") | |
.font(.body.bold()) | |
}) | |
Image("pic") | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
.frame(width: 35, height: 35) | |
.clipShape(Circle()) | |
.opacity(topBarOpacity) | |
Text("Lorem ipsum") | |
.fontWeight(.bold) | |
.foregroundColor(.white) | |
.opacity(topBarOpacity) | |
Spacer() | |
Button { | |
} label: { | |
Image(systemName: "line.3.horizontal.decrease") | |
.font(.body.bold()) | |
} | |
} | |
.padding(.horizontal) | |
.frame(height: 80) | |
.foregroundColor(.white) | |
.padding(.top, topEdge) | |
} | |
} | |
struct TopBar: View { | |
let topEdge: CGFloat | |
@Binding var offset: CGFloat | |
var maxHeight: CGFloat | |
var barOpacity: CGFloat { | |
let progress = -offset / 70 | |
let opactity = 1 - progress | |
return offset < 0 ? opactity : 1 | |
} | |
var body: some View { | |
VStack(alignment: .leading, spacing: 15) { | |
Image("pic") | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
.frame(width: 80, height: 80, alignment: .center) | |
.cornerRadius(10) | |
Text("Lorem ipsum") | |
.font(.largeTitle.bold()) | |
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") | |
.fontWeight(.semibold) | |
.foregroundColor(.white) | |
.opacity(0.8) | |
} | |
.padding() | |
.padding(.bottom) | |
.opacity(barOpacity) | |
} | |
} | |
struct MessageCardView: View { | |
var message: Message | |
var body: some View { | |
HStack(spacing: 15) { | |
Circle() | |
.fill(message.tintColor) | |
.frame(width: 50, height: 50, alignment: .center) | |
.opacity(0.8) | |
VStack(alignment: .leading, spacing: 8) { | |
Text(message.username) | |
.fontWeight(.bold) | |
Text(message.message) | |
.foregroundColor(.secondary) | |
} | |
.foregroundColor(.primary) | |
.frame(maxWidth: .infinity, alignment: .leading) | |
} | |
} | |
} | |
struct ContentView: View { | |
var body: some View { | |
GeometryReader { proxy in | |
HomeView(topEdge: proxy.safeAreaInsets.top) | |
.ignoresSafeArea(.all, edges: .top) | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} | |
@main | |
struct collapsableheaderApp: App { | |
var body: some Scene { | |
WindowGroup { | |
ContentView() | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment