SwiftUI Draggable Bottom Sheet
Class Custom Corner Radius
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)
}
}
BlurView(style: .systemThinMaterialDark)
.clipShape(CustomCorner(corners: [.topLeft, .topRight], radius: 30))
Advanced BlurView
struct BlurView: UIViewRepresentable {
var style: UIBlurEffect.Style
func makeUIView(context: Context) -> some UIVisualEffectView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: style))
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct Home: View {
@State var searchText = ""
// Gesture Properties...
@State var offset: CGFloat = 0
@State var lastOffset: CGFloat = 0
@GestureState var gestureOffset: CGFloat = 0
var body: some View {
ZStack {
// MARK: - BACKGROUND
// For getting Frame For Image
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
// print("first geometry height: \(frame.height)")
Image("image1")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: frame.width, height:frame.height)
}
.blur(radius: getBlurRadius())
.ignoresSafeArea()
// Bottom Sheet...
// MARK: - BOTTOM SHEET
// For Getting Height For Drag Gesture
GeometryReader { proxy -> AnyView in
let height = proxy.frame(in: .global).height
print("geometry height: \(height)")
print("screen height: \(UIScreen.main.bounds.height)")
return AnyView(
ZStack {
// MARK: - BOTTOM SHEET BACKGROUND
BlurView(style: .systemThinMaterialDark)
.clipShape(CustomCorner(corners: [.topLeft, .topRight], radius: 30))
VStack {
// MARK: - TOP DRAG INDICATOR
Capsule()
.fill(.white)
.frame(width: 60, height: 4)
.padding(.top)
// MARK: - CUSTOM CONTENT
// Content
Spacer()
} //: VSTACK
} //: ZSTACK
.offset(y: height - 100)
.offset(y: -offset > 0 ? -offset <= (height - 100) ? offset : -(height - 100) : 0)
.gesture(DragGesture().updating($gestureOffset, body: { value, out, _ in
out = value.translation.height
onChange()
}).onEnded({ value in
let maxHeight = height - 100
withAnimation {
// offset = 0
// Logic Conditions For Moving States....
// Up down or mid...
if -offset > 100 && -offset < maxHeight / 2 {
// Mid...
offset = -(maxHeight / 3)
} else if -offset > maxHeight / 2 {
offset = -maxHeight
} else {
offset = 0
}
}
// Storing Last Offset...
// So that the gesture can contine from the last position....
lastOffset = offset
}))
)
}
.ignoresSafeArea(.all, edges: .bottom)
}
}
func onChange() {
DispatchQueue.main.async {
self.offset = gestureOffset + lastOffset
}
}
// Blur Radius for BG...
func getBlurRadius() -> CGFloat {
let progress = -offset / (UIScreen.main.bounds.height - 100)
return progress * 30
}
}
Draggable BottomSheet
//
// ContentView.swift
// BottomSheetDrawer
//
// Created by paige on 2021/11/07.
//
import SwiftUI
struct ContentView: View {
var body: some View {
Home()
}
}
struct Home: View {
@State var searchText = ""
// Gesture Properties...
@State var offset: CGFloat = 0
@State var lastOffset: CGFloat = 0
@GestureState var gestureOffset: CGFloat = 0
var body: some View {
ZStack {
// MARK: - BACKGROUND
// For getting Frame For Image
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
Image("image1")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: frame.width, height:frame.height)
}
.blur(radius: getBlurRadius())
.ignoresSafeArea()
// Bottom Sheet...
// MARK: - BOTTOM SHEET
// For Getting Height For Drag Gesture
GeometryReader { proxy -> AnyView in
let height = proxy.frame(in: .global).height
print("geometry height: \(height)")
print("screen height: \(UIScreen.main.bounds.height)")
return AnyView(
ZStack {
// MARK: - BOTTOM SHEET BACKGROUND
BlurView(style: .systemThinMaterialDark)
.clipShape(CustomCorner(corners: [.topLeft, .topRight], radius: 30))
VStack {
// MARK: - TOP DRAG INDICATOR
Capsule()
.fill(.white)
.frame(width: 60, height: 4)
.padding(.top)
// MARK: - CUSTOM CONTENT
// Content
Spacer()
} //: VSTACK
} //: ZSTACK
.offset(y: height - 100)
.offset(y: -offset > 0 ? -offset <= (height - 100) ? offset : -(height - 100) : 0)
.gesture(DragGesture().updating($gestureOffset, body: { value, out, _ in
out = value.translation.height
print("offset: \(offset)")
onChange()
}).onEnded({ value in
let maxHeight = height - 100
withAnimation {
// offset = 0
// Logic Conditions For Moving States....
// Up down or mid...
if -offset > 100 && -offset < maxHeight / 2 {
// Mid...
offset = -(maxHeight / 3)
} else if -offset > maxHeight / 2 {
offset = -maxHeight
} else {
offset = 0
}
}
// Storing Last Offset...
// So that the gesture can contine from the last position....
lastOffset = offset
}))
)
}
.ignoresSafeArea(.all, edges: .bottom)
}
}
func onChange() {
DispatchQueue.main.async {
self.offset = gestureOffset + lastOffset
}
}
// Blur Radius for BG...
func getBlurRadius() -> CGFloat {
let progress = -offset / (UIScreen.main.bounds.height - 100)
return progress * 30
}
}
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 BlurView: UIViewRepresentable {
var style: UIBlurEffect.Style
func makeUIView(context: Context) -> some UIVisualEffectView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: style))
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
Entire Code
//
// ContentView.swift
// BottomSheetDrawer
//
// Created by paige on 2021/11/07.
//
import SwiftUI
struct ContentView: View {
var body: some View {
Home()
}
}
struct Home: View {
@State var searchText = ""
// Gesture Properties...
@State var offset: CGFloat = 0
@State var lastOffset: CGFloat = 0
@GestureState var gestureOffset: CGFloat = 0
var body: some View {
ZStack {
// MARK: - BACKGROUND
// For getting Frame For Image
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
// print("first geometry height: \(frame.height)")
Image("image1")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: frame.width, height:frame.height)
}
.blur(radius: getBlurRadius())
.ignoresSafeArea()
// Bottom Sheet...
// MARK: - BOTTOM SHEET
// For Getting Height For Drag Gesture
GeometryReader { proxy -> AnyView in
let height = proxy.frame(in: .global).height
print("geometry height: \(height)")
print("screen height: \(UIScreen.main.bounds.height)")
return AnyView(
ZStack {
// MARK: - BOTTOM SHEET BACKGROUND
BlurView(style: .systemThinMaterialDark)
.clipShape(CustomCorner(corners: [.topLeft, .topRight], radius: 30))
VStack {
// MARK: - TOP DRAG INDICATOR
Capsule()
.fill(.white)
.frame(width: 60, height: 4)
.padding(.top)
// MARK: - CUSTOM CONTENT
// Content
TextField("Search", text: $searchText)
.padding(.vertical, 10)
.padding(.horizontal)
// .background(BlurView(style: .dark))
.cornerRadius(20)
.colorScheme(.dark)
.padding(.top, 10)
VStack {
HStack {
Text("Favorites")
.fontWeight(.bold)
.foregroundColor(.white)
Spacer()
Button {
} label: {
Text("See All")
.fontWeight(.bold)
.foregroundColor(.gray)
}
}
.padding(.top, 20)
Divider()
.background(.white)
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15) {
VStack(spacing: 8) {
Button {
} label: {
Image(systemName: "house.fill")
.font(.title)
.frame(width: 50, height: 50)
.background(BlurView(style: .dark))
.clipShape(Circle())
}
Text("Home")
.foregroundColor(.white)
}
VStack(spacing: 8) {
Button {
} label: {
Image(systemName: "briefcase.fill")
.font(.title)
.frame(width: 50, height: 50)
.background(BlurView(style: .dark))
.clipShape(Circle())
}
Text("Work")
.foregroundColor(.white)
}
VStack(spacing: 8) {
Button {
} label: {
Image(systemName: "house.fill")
.font(.title)
.frame(width: 50, height: 50)
.background(BlurView(style: .dark))
.clipShape(Circle())
}
Text("Add")
.foregroundColor(.white)
}
}
} //: ScrollView
.padding(.top)
HStack {
Text("Editor's Pick")
.fontWeight(.bold)
.foregroundColor(.white)
Spacer()
Button {
} label: {
Text("See All")
.fontWeight(.bold)
.foregroundColor(.gray)
}
}
.padding(.top, 25)
Divider()
.background(.white)
ScrollView(.vertical, showsIndicators: false) {
ForEach(1...5, id: \.self) { index in
Image("image\(index)")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width - 30, height: 250)
.cornerRadius(15)
.padding(.top)
}
}
Spacer()
} //: VSTACK
.padding(16)
} //: ZSTACK
.offset(y: height - 100)
.offset(y: -offset > 0 ? -offset <= (height - 100) ? offset : -(height - 100) : 0)
.gesture(DragGesture().updating($gestureOffset, body: { value, out, _ in
out = value.translation.height
onChange()
}).onEnded({ value in
let maxHeight = height - 100
withAnimation {
// offset = 0
// Logic Conditions For Moving States....
// Up down or mid...
if -offset > 100 && -offset < maxHeight / 2 {
// Mid...
offset = -(maxHeight / 3)
} else if -offset > maxHeight / 2 {
offset = -maxHeight
} else {
offset = 0
}
}
// Storing Last Offset...
// So that the gesture can contine from the last position....
lastOffset = offset
}))
)
}
.ignoresSafeArea(.all, edges: .bottom)
}
}
func onChange() {
DispatchQueue.main.async {
self.offset = gestureOffset + lastOffset
}
}
// Blur Radius for BG...
func getBlurRadius() -> CGFloat {
let progress = -offset / (UIScreen.main.bounds.height - 100)
return progress * 30
}
}
struct BlurView: UIViewRepresentable {
var style: UIBlurEffect.Style
func makeUIView(context: Context) -> some UIVisualEffectView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: style))
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
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)
}
}