Skip to content

Instantly share code, notes, and snippets.

@ahmedAlmasri
Created May 1, 2023 10:42
Show Gist options
  • Save ahmedAlmasri/c1f0c57f5de80a0d05471e671c604d30 to your computer and use it in GitHub Desktop.
Save ahmedAlmasri/c1f0c57f5de80a0d05471e671c604d30 to your computer and use it in GitHub Desktop.
//
// BottomSheet.swift
//
//
// Created by Ahmad Almasri on 21/01/2023.
//
import SwiftUI
import SwiftUIX
struct BottomSheet<Content: View>: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Binding private var isPresented: Bool
@State private var showSheetContent = false
@GestureState private var translation: CGFloat = 0
private let content: Content
private var hideShowDuration: DispatchTime {
DispatchTime.now() + 0.4
}
init(isPresented: Binding<Bool>, @ViewBuilder content: () -> Content) {
self._isPresented = isPresented
self.content = content()
}
var body: some View {
GeometryReader { geo in
VStack(spacing: 0) {
content
.padding(.horizontal, horizontalSizeClass == .regular ? 32 : 0)
.padding(.horizontal)
}
.frame(width: geo.size.width, alignment: .top)
.background(Color.raisinBlack)
.cornerRadius(20)
.frame(height: geo.size.height, alignment: .bottom)
.offset(y: max((showSheetContent ? 0 : geo.size.height), translation))
.background(sheetBackground)
.animation(.interactiveSpring(), value: translation)
.gesture(
DragGesture().updating($translation, body: { value, state, _ in
state = value.translation.height
})
.onEnded({ value in
let snapDistance = geo.size.height * 0.1
guard abs(value.translation.height) > snapDistance else {
return
}
let presented = value.translation.height < 0
if !presented {
dismiss()
}
})
)
}
.onAppear { presentSheetContent() }
.edgesIgnoringSafeArea(.all)
}
private var sheetBackground: some View {
Color.black.opacity(showSheetContent ? 0.5 : 0)
.onTapGesture { dismiss() }
}
// MARK: - Actions
private func dismiss() {
withAnimation(.easeOut) {
self.showSheetContent = false
DispatchQueue.main.asyncAfter(deadline: hideShowDuration) {
self.isPresented = false
}
}
}
private func presentSheetContent() {
withAnimation(.spring(response: 0.3, dampingFraction: 1, blendDuration: 0)) {
self.showSheetContent = true
}
}
}
public extension View {
func bottomSheet<Content: View>(isPresented: Binding<Bool>, title: LocalizedStringKey? = nil, @ViewBuilder content: @escaping () -> Content) -> some View {
self.windowOverlay(isKeyAndVisible: isPresented) {
if isPresented.wrappedValue {
BottomSheet(isPresented: isPresented) {
HStack(spacing: 16) {
ZStack {
Circle()
.fill(Color.darkCharcoal)
.frame(width: 32, height: 32)
Image(systemName: "xmark")
.foregroundColor(.light)
}
.onTapGesture {
withAnimation(.easeOut) {
isPresented.wrappedValue = false
}
}
if let title = title {
Text(title)
.foregroundColor(.light)
.font(.h2)
}
Spacer()
}
.padding(.vertical)
content()
.padding(.bottom, 32)
}
}
}
}
}
#if DEBUG
struct BottomSheet_Previews: PreviewProvider {
static var previews: some View {
BottomSheetView()
}
struct BottomSheetView: View {
@State var isShow = false
var body: some View {
Button(action: {
isShow = true
}, label: {
Text("Show sheet")
})
.bottomSheet(isPresented: $isShow, title: "Test") {
HStack {
VStack(alignment: .leading) {
Label("Info sheet ready", systemImage: "checkmark")
Label("Custom content", systemImage: "checkmark")
Label("Adaptive height", systemImage: "checkmark")
}
.foregroundColor(.light)
Spacer()
}
}
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment