Hi! this is TDCIAN!
Let's find out about how to make bottom sheet drawer that move with gesture!
Look at these codes!
(1) Home.swift
import SwiftUI | |
struct Home: View { | |
// Search text binding value | |
@State var searchText: String = "" | |
// Gesture properties | |
@State var offset: CGFloat = 0 | |
@State var lastOffset: CGFloat = 0 | |
@GestureState var gestureOffset: CGFloat = 0 | |
var body: some View { | |
ZStack { | |
// For getting frame for image | |
GeometryReader { proxy in | |
let frame = proxy.frame(in: .global) | |
Image("bg") | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
.frame(width: frame.width, height: frame.height) | |
} | |
.blur(radius: getBlurRadius()) // BlurRadius change depends on offset | |
.ignoresSafeArea() | |
// For getting height for drag gesture | |
GeometryReader { proxy -> AnyView in | |
let height = proxy.frame(in: .global).height | |
return AnyView( | |
ZStack { | |
// Bottom Sheet | |
BlurView(style: .systemThinMaterialDark) | |
.clipShape(CustomCorner(corners: [.topLeft, .topRight], radius: 30)) | |
VStack { | |
VStack { | |
Capsule() | |
.fill(Color.white) | |
.frame(width: 60, height: 4) | |
TextField("Search", text: $searchText) | |
.padding(.vertical, 10) | |
.padding(.horizontal) | |
.background(BlurView(style: .dark)) | |
.cornerRadius(10) | |
.colorScheme(.dark) | |
.padding(.top, 10) | |
} | |
.frame(height: 100) | |
.border(Color.red, width: 1) | |
// ScrollView content | |
ScrollView(.vertical, showsIndicators: false, content: { | |
BottomContent() | |
}) | |
.border(Color.blue, width: 1) | |
} | |
.padding(.horizontal) | |
.frame(maxHeight: .infinity, alignment: .top) | |
} | |
.border(Color.green, width: 1) | |
.offset(y: height - 100) | |
.offset(y: -offset > 0 ? -offset <= (height - 100) ? offset : -(height - 100) : 0) | |
.gesture(DragGesture().updating($gestureOffset, body: { value, state, transcation in | |
state = value.translation.height | |
onChange() | |
}).onEnded({ value in | |
let maxHeight = height - 100 | |
withAnimation { | |
// (1) Mid | |
if -offset > 100 && -offset < maxHeight / 2 { | |
// * Test each case! | |
offset = -(maxHeight / 4) | |
// offset = -(maxHeight / 3) | |
// offset = -(maxHeight / 2) | |
// offset = -(maxHeight / 1.5) | |
// offset = -(maxHeight / 1.25) | |
// (2) Top | |
} else if -offset > maxHeight / 2 { | |
offset = -maxHeight | |
// (3) Bottom | |
} else { | |
offset = 0 | |
} | |
} | |
// Storing last offset, so that the gesture can continue from the last position | |
lastOffset = offset | |
})) | |
) | |
} | |
.ignoresSafeArea(.all, edges: .bottom) | |
} | |
} | |
func onChange() { | |
DispatchQueue.main.async { | |
self.offset = gestureOffset + lastOffset | |
} | |
} | |
// Blur radius for background | |
func getBlurRadius() -> CGFloat { | |
let progress = -offset / (UIScreen.main.bounds.height - 100) | |
return progress * 30 | |
} | |
} | |
struct Home_Previews: PreviewProvider { | |
static var previews: some View { | |
Home() | |
} | |
} | |
struct BottomContent: View { | |
var body: some View { | |
VStack { | |
HStack { | |
Text("Favorite") | |
.fontWeight(.bold) | |
.foregroundColor(.white) | |
Spacer() | |
Button(action: { | |
}, label: { | |
Text("See All") | |
.fontWeight(.bold) | |
.foregroundColor(.gray) | |
}) | |
} | |
.padding(.top, 20) | |
Divider() | |
.background(Color.white) | |
ScrollView(.horizontal, showsIndicators: false, content: { | |
HStack(spacing: 15) { | |
VStack(spacing: 8) { | |
Button(action: { | |
}, label: { | |
Image(systemName: "house.fill") | |
.font(.title) | |
.frame(width: 65, height: 65) | |
.background(BlurView(style: .dark)) | |
.clipShape(Circle()) | |
}) | |
Text("Home") | |
.foregroundColor(Color.white) | |
} | |
VStack(spacing: 8) { | |
Button(action: { | |
}, label: { | |
Image(systemName: "briefcase.fill") | |
.font(.title) | |
.frame(width: 65, height: 65) | |
.background(BlurView(style: .dark)) | |
.clipShape(Circle()) | |
}) | |
Text("Work") | |
.foregroundColor(Color.white) | |
} | |
VStack(spacing: 8) { | |
Button(action: { | |
}, label: { | |
Image(systemName: "plus") | |
.font(.title) | |
.frame(width: 65, height: 65) | |
.background(BlurView(style: .dark)) | |
.clipShape(Circle()) | |
}) | |
Text("Add") | |
.foregroundColor(Color.white) | |
} | |
} | |
}) | |
.padding(.top) | |
HStack { | |
Text("Editor's Pick") | |
.fontWeight(.bold) | |
.foregroundColor(.white) | |
Spacer() | |
Button(action: { | |
}, label: { | |
Text("See All") | |
.fontWeight(.bold) | |
.foregroundColor(.gray) | |
}) | |
} | |
.padding(.top, 25) | |
Divider() | |
.background(Color.white) | |
ForEach(1...6, id: \.self) { index in | |
Image("p\(index)") | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
.frame(width: UIScreen.main.bounds.width - 30, height: 250) | |
.cornerRadius(15) | |
.padding(.top) | |
} | |
} | |
} | |
} |
(2) BlurView.swift
import SwiftUI | |
struct BlurView: UIViewRepresentable { | |
var style: UIBlurEffect.Style | |
func makeUIView(context: Context) -> UIVisualEffectView { | |
let view = UIVisualEffectView(effect: UIBlurEffect(style: style)) | |
return view | |
} | |
func updateUIView(_ uiView: UIVisualEffectView, context: Context) { | |
} | |
} |
(3) CustomCorner.swift
import SwiftUI | |
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) | |
} | |
} |
(4) ContentView.swift
import SwiftUI | |
struct ContentView: View { | |
var body: some View { | |
Home() | |
} | |
} |
(5) In simulator



Full source code: https://github.com/TDCIAN/SwiftUIBottomSheetDrawer
GitHub - TDCIAN/SwiftUIBottomSheetDrawer: based on: https://www.youtube.com/watch?v=CyMtjSspJZA
based on: https://www.youtube.com/watch?v=CyMtjSspJZA - GitHub - TDCIAN/SwiftUIBottomSheetDrawer: based on: https://www.youtube.com/watch?v=CyMtjSspJZA
github.com
Reference: https://www.youtube.com/watch?v=CyMtjSspJZA
Images Reference: https://unsplash.com/
Beautiful Free Images & Pictures | Unsplash
Beautiful, free images and photos that you can download and use for any project. Better than any royalty free or stock photos.
unsplash.com
'iOS > SwiftUI' 카테고리의 다른 글
[iOS / SwiftUI] DownsizedImage (0) | 2025.03.03 |
---|---|
[iOS / SwiftUI] SwiftUI Notification Deep Linking (0) | 2025.02.16 |
[iOS / SwiftUI] SwiftUI Custom Alerts (0) | 2025.02.08 |
[iOS / SwiftUI] Use Deep Link in SwiftUI (0) | 2022.01.11 |
[iOS / SwiftUI] Check Network Connection Status with NWPathMonitor (0) | 2021.12.30 |